/********************************************************************************

   Fotoxx      edit photos and manage collections

   Copyright 2007-2020 Michael Cornelison
   source code URL: https://kornelix.net
   contact: mkornelix@gmail.com

   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, either version 3 of the License, or
   (at your option) any later version. See https://www.gnu.org/licenses

   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.

*********************************************************************************

   Fotoxx image edit - warp menu functions

   m_unbend                straighten curvature added by pano or improve perspective
   m_perspective           select a tetragon and convert into a rectangle
   m_warp_area             select an area and warp interior with mouse drags
   m_warp_curved           warp an image or area using a curved transform
   m_warp_linear           warp an image using a linear transform
   m_warp_affine           warp an image using an affine transform
   m_unwarp_closeup        select a face in a close-up photo, remove distortion
   m_flatbook              flatten a photographed book page
   m_area_rescale          rescale while leaving selected areas unchanged
   m_waves                 distort an image with a wave pattern
   m_twist                 twist image centered at mouse position
   m_sphere                sphere image projection, image shrinks from center to edge
   m_stretch               image scale expands from center to edge
   m_inside_out            turn an image inside-out
   m_tiny_planet           wrap a panorama image around a circle
   m_escher_spiral         generate inward-spiraling recursive image

*********************************************************************************/

#define EX extern                                                                //  enable extern declarations
#include "fotoxx.h"                                                              //  (variables in fotoxx.h are refs)

/********************************************************************************/


//  unbend an image
//  straighten curvature added by pano or improve perspective

float    unbend_lin_horz, unbend_lin_vert;                                       //  unbend values from dialog
float    unbend_cur_horz, unbend_cur_vert;
float    unbend_x1, unbend_x2, unbend_y1, unbend_y2;                             //  unbend axes scaled 0 to 1
int      unbend_hx1, unbend_hy1, unbend_hx2, unbend_hy2;
int      unbend_vx1, unbend_vy1, unbend_vx2, unbend_vy2;

editfunc    EFunbend;


//  menu function

void m_unbend(GtkWidget *, cchar *)                                              //  overhauled
{
   int    unbend_dialog_event(zdialog* zd, cchar *event);
   void * unbend_thread(void *);
   void   unbend_mousefunc();

   F1_help_topic = "unbend";

   EFunbend.funcname = "unbend";
   EFunbend.FprevReq = 1;                                                        //  use preview
   EFunbend.Frestart = 1;                                                        //  restart allowed
   EFunbend.threadfunc = unbend_thread;                                          //  thread function
   EFunbend.mousefunc = unbend_mousefunc;                                        //  mouse function
   if (! edit_setup(EFunbend)) return;                                           //  setup edit
   
   PXM_addalpha(E0pxm);
   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

/***
          _______________________
         |       Unbend           |
         |   ______               |
         |  |      |   [0__]      |
         |  |      |              |
         |  |______|              |
         |                        |
         |   ______               |
         |  |      |   [0__]      |
         |  |      |              |
         |  |______|              |
         |                        |
         |   ______               |
         |  |      |   [0__]      |
         |  |      |              |
         |  |______|              |
         |                        |
         |   ______               |
         |  |      |   [0__]      |
         |  |      |              |
         |  |______|              |
         |                        |
         |                        |
         | [grid] [done] [cancel] |
         |________________________|

***/

   zdialog *zd = zdialog_new(E2X("Unbend"),Mwin,Bgrid,Bdone,Bcancel,null);
   EFunbend.zd = zd;
   
   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=10|homog");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"space=10|homog");

   zdialog_add_widget(zd,"icon","VL","vb1","unbend vert linear.png","size=64");
   zdialog_add_widget(zd,"icon","VC","vb1","unbend vert curved.png","size=64");
   zdialog_add_widget(zd,"icon","HL","vb1","unbend horz linear.png","size=64");
   zdialog_add_widget(zd,"icon","HC","vb1","unbend horz curved.png","size=64");

   zdialog_add_widget(zd,"zspin","splinvert","vb2","-99|99|1|0");
   zdialog_add_widget(zd,"zspin","spcurvert","vb2","-99|99|1|0");
   zdialog_add_widget(zd,"zspin","splinhorz","vb2","-99|99|1|0");
   zdialog_add_widget(zd,"zspin","spcurhorz","vb2","-99|99|1|0");

   unbend_x1 = unbend_x2 = unbend_y1 = unbend_y2 = 0.5;                          //  initial axes thru image middle
   unbend_lin_horz = unbend_lin_vert = 0;
   unbend_cur_horz = unbend_cur_vert = 0;

   currgrid = 2;                                                                 //  use unbend grid
   takeMouse(unbend_mousefunc,dragcursor);                                       //  connect mouse function
   signal_thread();
   zdialog_run(zd,unbend_dialog_event,"save");                                   //  run dialog, parallel
   return;
}


//  dialog event and completion callback function

int unbend_dialog_event(zdialog *zd, cchar *event)
{
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()

   if (zd->zstat)                                                                //  dialog complete
   {
      currgrid = 0;                                                              //  restore normal grid settings

      if (zd->zstat == 1) {                                                      //  toggle grid
         zd->zstat = 0;
         m_gridlines(0,"grid 2");                                                //  grid settings dialog
         return 1;
      }

      else if (zd->zstat == 2) {                                                 //  done
         CEF->Fmods = 0;
         if (unbend_cur_vert || unbend_cur_horz ||                               //  image3 modified
             unbend_lin_vert || unbend_lin_horz) {
            CEF->Fmods = 1;
            CEF->Fsaved = 0;                                                     //  done
         }
         edit_fullsize();                                                        //  get full size image
         signal_thread();
         edit_done(0);                                                           //  commit edit
      }
      
      else edit_cancel(0);                                                       //  cancel, discard edit
      draw_toplines(2,0);                                                        //  erase axes-lines
      return 1;
   }

   if (strmatch(event,"splinvert")) {                                            //  get new unbend value
      zdialog_fetch(zd,"splinvert",unbend_lin_vert);
      signal_thread();                                                           //  trigger thread
   }

   if (strmatch(event,"splinhorz")) {
      zdialog_fetch(zd,"splinhorz",unbend_lin_horz);
      signal_thread();
   }

   if (strmatch(event,"spcurvert")) {
      zdialog_fetch(zd,"spcurvert",unbend_cur_vert);
      signal_thread();
   }

   if (strmatch(event,"spcurhorz")) {
      zdialog_fetch(zd,"spcurhorz",unbend_cur_horz);
      signal_thread();
   }

   return 1;
}


//  unbend mouse function                                                        //  adjustable axes

void unbend_mousefunc()
{
   cchar       *close;
   float       dist1, dist2;
   float       mpx = 0, mpy = 0;

   if (LMclick) {                                                                //  left mouse click
      mpx = Mxclick;
      mpy = Myclick;
   }

   if (Mxdrag || Mydrag) {                                                       //  mouse dragged
      mpx = Mxdrag;
      mpy = Mydrag;
   }

   if (! mpx && ! mpy) return;

   mpx = 1.0 * mpx / E3pxm->ww;                                                  //  scale mouse position 0 to 1
   mpy = 1.0 * mpy / E3pxm->hh;

   if (mpx < 0.2 || mpx > 0.8 ) {                                                //  check reasonable position
      if (mpy < 0.1 || mpy > 0.9) return;
   }
   else if (mpy < 0.2 || mpy > 0.8) {
      if (mpx < 0.1 || mpx > 0.9) return;
   }
   else return;

   close = "?";                                                                  //  find closest axis end-point
   dist1 = 2;

   dist2 = mpx * mpx + (mpy-unbend_y1) * (mpy-unbend_y1);
   if (dist2 < dist1) {
      dist1 = dist2;
      close = "left";
   }

   dist2 = (1-mpx) * (1-mpx) + (mpy-unbend_y2) * (mpy-unbend_y2);
   if (dist2 < dist1) {
      dist1 = dist2;
      close = "right";
   }

   dist2 = (mpx-unbend_x1) * (mpx-unbend_x1) + mpy * mpy;
   if (dist2 < dist1) {
      dist1 = dist2;
      close = "top";
   }

   dist2 = (mpx-unbend_x2) * (mpx-unbend_x2) + (1-mpy) * (1-mpy);
   if (dist2 < dist1) {
      dist1 = dist2;
      close = "bottom";
   }

   if (strmatch(close,"left")) unbend_y1 = mpy;                                  //  set new axis end-point
   if (strmatch(close,"right")) unbend_y2 = mpy;
   if (strmatch(close,"top")) unbend_x1 = mpx;
   if (strmatch(close,"bottom")) unbend_x2 = mpx;

   signal_thread();                                                              //  trigger thread

   LMclick = Mxdrag = Mydrag = 0;
   return;
}


//  unbend thread function

void * unbend_thread(void *arg)
{
   void * unbend_wthread(void *);

   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      unbend_hx1 = 0;                                                            //  scale axes to E3ww/hh
      unbend_hy1 = unbend_y1 * E3pxm->hh;
      unbend_hx2 = E3pxm->ww;
      unbend_hy2 = unbend_y2 * E3pxm->hh;

      unbend_vx1 = unbend_x1 * E3pxm->ww;
      unbend_vy1 = 0;
      unbend_vx2 = unbend_x2 * E3pxm->ww;
      unbend_vy2 = E3pxm->hh;

      Ntoplines = 2;
      toplines[0].x1 = unbend_hx1;                                               //  lines on window
      toplines[0].y1 = unbend_hy1;
      toplines[0].x2 = unbend_hx2;
      toplines[0].y2 = unbend_hy2;
      toplines[0].type = 2;
      toplines[1].x1 = unbend_vx1;
      toplines[1].y1 = unbend_vy1;
      toplines[1].x2 = unbend_vx2;
      toplines[1].y2 = unbend_vy2;
      toplines[1].type = 2;

      do_wthreads(unbend_wthread,NWT);                                           //  worker threads

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop g++ warning
}


void * unbend_wthread(void *arg)                                                 //  worker thread function
{
   int         index = *((int *) arg);
   int         vstat, px3, py3, cx3, cy3;
   float       dispx, dispy, dispx2, dispy2;
   float       px1, py1, vx1, vx2, hy1, hy2;
   float       curvert, curhorz, linvert, linhorz;
   float       vpix[4], *pix3;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   curvert = int(unbend_cur_vert * 0.01 * E3pxm->hh);                            //  -0.99 to +0.99
   curhorz = int(unbend_cur_horz * 0.01 * E3pxm->ww);
   linvert = int(unbend_lin_vert * 0.0013 * E3pxm->hh);                          //  -0.13 to +0.13
   linhorz = int(unbend_lin_horz * 0.0013 * E3pxm->ww);

   vx1 = unbend_vx1;
   vx2 = unbend_vx2;
   hy1 = unbend_hy1;
   hy2 = unbend_hy2;
   
   for (py3 = index; py3 < E3pxm->hh; py3 += NWT)                                //  step through F3 pixels
   for (px3 = 0; px3 < E3pxm->ww; px3++)
   {
      cx3 = vx1 + (vx2 - vx1) * py3 / E3pxm->hh;                                 //  center of unbend
      cy3 = hy1 + (hy2 - hy1) * px3 / E3pxm->ww;
      dispx = 2.0 * (px3 - cx3) / E3pxm->ww;                                     //  -1.0 ..  0.0 .. +1.0  (roughly)
      dispy = 2.0 * (py3 - cy3) / E3pxm->hh;                                     //  -1.0 ..  0.0 .. +1.0
      dispx2 = -cosf(0.8 * dispx) + 1;                                           //   curved
      dispy2 = -cosf(0.8 * dispy) + 1;

      pix3 = PXMpix(E3pxm,px3,py3);                                              //  output pixel
      px1 = px3;                                                                 //  input pixel = output
      py1 = py3;

      px1 += dispx * dispy * linhorz;                                            //  move input pixel
      py1 += dispy * dispx * linvert;
      px1 -= dispx * dispy2 * curhorz;                                           //  change sign
      py1 -= dispy * dispx2 * curvert;

      vstat = vpixel(E1pxm,px1,py1,vpix);
      if (vstat) memcpy(pix3,vpix,pcc);
      else memset(pix3,0,pcc);
   }

   pthread_exit(0);
}


/********************************************************************************/

//  Convert a selected tetragon area into a rectangle, converting the
//  rest of the image to match and keeping straight lines straight.

namespace perspective
{
   int         PSP_pixel[4][2];                                                  //  last 0-4 pixels clicked
   int         PSP_npix;                                                         //  count of pixels
   char        PSP_pixlab[4][4] = { " A ", " B ", " C ", " D " };
   int         PSP_corner = 0;

   editfunc    EFperspective;

   int   dialog_event(zdialog *zd, cchar *event);
   void  mousefunc(void);
   void  warpfunc(void);
   void  KBfunc(int key);
}


//  menu function

void m_perspective(GtkWidget *, cchar *)
{
   using namespace perspective;

   cchar  *PSP_message = E2X(
          " Click the four corners of a tetragon area. Press [apply]. \n"
          " The image is warped to make the tetragon into a rectangle.");

   F1_help_topic = "perspective";

   EFperspective.menufunc = m_perspective;
   EFperspective.funcname = "perspective";
   EFperspective.Frestart = 1;                                                   //  restart allowed
   EFperspective.mousefunc = mousefunc;                                          //  mouse function
   if (! edit_setup(EFperspective)) return;                                      //  setup edit

   PXM_addalpha(E0pxm);
   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

   PSP_npix = 0;                                                                 //  no pixels yet

   zdialog *zd = zdialog_new(E2X("Perspective Correction"),Mwin,Bapply,Breset,Btrim,Bdone,null);
   zdialog_add_widget(zd,"label","lab1","dialog",PSP_message,"space=3");

   EFperspective.zd = zd;
   zdialog_run(zd,dialog_event,"save");                                          //  run dialog, parallel

   takeMouse(mousefunc,dragcursor);                                              //  connect mouse function
   return;
}


//  dialog completion callback function

int perspective::dialog_event(zdialog *zd, cchar *event)
{
   using namespace perspective;

   int      ii, px, py;
   int      minx, maxx, miny, maxy;

   if (strmatch(event,"done")) zd->zstat = 3;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 4;                                  //  from f_open()

   if (strmatch(event,"focus"))                                                  //  toggle mouse capture   12.01
      takeMouse(mousefunc,dragcursor);

   if (! zd->zstat) return 1;                                                    //  wait for completion

   if (zd->zstat == 1)                                                           //  apply
   {
      erase_toptext(102);                                                        //  erase points
      warpfunc();                                                                //  do the warp
      zd->zstat = 0;                                                             //  keep dialog active
   }

   else if (zd->zstat == 2)                                                      //  reset
   {
      edit_reset();
      zd->zstat = 0;

      for (ii = 0; ii < PSP_npix; ii++)                                          //  show pixel labels on image
      {
         px = PSP_pixel[ii][0];
         py = PSP_pixel[ii][1];
         add_toptext(102,px,py,PSP_pixlab[ii],"Sans 8");
      }
      Fpaint2();
   }
   
   else if (zd->zstat == 3)                                                      //  trim 
   {
      if (PSP_npix < 4) {                                                        //  wait for 4 corners
         zd->zstat = 0;
         return 1;
      }
      
      erase_toptext(102);                                                        //  erase labels
      warpfunc();                                                                //  do the warp
      edit_done(0);

      minx = miny = 99999;                                                       //  find trim limits
      maxx = maxy = 0;
      
      for (ii = 0; ii < 4; ii++) {
         if (PSP_pixel[ii][0] < minx) minx = PSP_pixel[ii][0];
         if (PSP_pixel[ii][0] > maxx) maxx = PSP_pixel[ii][0];
         if (PSP_pixel[ii][1] < miny) miny = PSP_pixel[ii][1];
         if (PSP_pixel[ii][1] > maxy) maxy = PSP_pixel[ii][1];
      }

      trimx1 = minx;                                                             //  set parameters for trim function
      trimy1 = miny;
      trimx2 = maxx;
      trimy2 = maxy;
      m_trim_rotate(0,"keep");
   }

   else if (zd->zstat == 4) {                                                    //  done
      erase_toptext(102);                                                        //  erase labels
      edit_done(0);                                                              //  commit edit
   }

   else {                                                                        //  cancel
      erase_toptext(102);                                                        //  erase labels
      edit_cancel(0);                                                            //  discard edit
   }

   return 1;
}


//  mouse function - click on 4 corners of tetragon

void perspective::mousefunc()
{
   using namespace perspective;

   int         ii, minii, jj, px, py;
   float       dist, distx, disty, mindist;

   if (LMclick)                                                                  //  left click
   {
      for (ii = 0; ii < PSP_npix; ii++)                                          //  check if very near a previous corner
      {
         if (abs(PSP_pixel[ii][0] - Mxclick) < 0.07 * E3pxm->ww
          && abs(PSP_pixel[ii][1] - Myclick) < 0.07 * E3pxm->hh)
         {
            PSP_pixel[ii][0] = Mxclick;                                          //  yes, set new corner position
            PSP_pixel[ii][1] = Myclick;
            PSP_corner = ii;
            goto showcorners;
         }
      }

      if (PSP_npix < 4)                                                          //  if < 4 corners, add a new one
      {
         ii = PSP_npix;                                                          //  next corner to fill
         PSP_pixel[ii][0] = Mxclick;                                             //  save newest corner position
         PSP_pixel[ii][1] = Myclick;
         PSP_corner = ii;
         PSP_npix++;
         goto showcorners;
      }

      mindist = 99999;                                                           //  all 4 corners already specified
      minii = -1;

      for (ii = 0; ii < 4; ii++)                                                 //  find closest corner to click position
      {
         distx = (Mxclick - PSP_pixel[ii][0]);
         disty = (Myclick - PSP_pixel[ii][1]);
         dist = sqrt(distx*distx + disty*disty);
         if (dist < mindist) {
            mindist = dist;
            minii = ii;
         }
      }

      if (minii >= 0) {                                                          //  set new corner position
         ii = minii;
         PSP_pixel[ii][0] = Mxclick;
         PSP_pixel[ii][1] = Myclick;
         PSP_corner = ii;
         goto showcorners;
      }
   }

   else if (RMclick)                                                             //  right click
   {
      mindist = 99999;
      minii = -1;

      for (ii = 0; ii < PSP_npix; ii++)                                          //  find closest corner to click position
      {
         distx = (Mxclick - PSP_pixel[ii][0]);
         disty = (Myclick - PSP_pixel[ii][1]);
         dist = sqrt(distx*distx + disty*disty);
         if (dist < mindist) {
            mindist = dist;
            minii = ii;
         }
      }

      if (minii >= 0) {                                                          //  replace deleted corner with
         ii = minii;                                                             //    last corner
         jj = PSP_npix - 1;
         PSP_pixel[ii][0] = PSP_pixel[jj][0];
         PSP_pixel[ii][1] = PSP_pixel[jj][1];
         PSP_corner = ii;
         --PSP_npix;                                                             //  reduce corner count
         goto showcorners;
      }
   }

showcorners:                                                                     //  show corner labels on image

   erase_toptext(102);

   for (ii = 0; ii < PSP_npix; ii++)
   {
      px = PSP_pixel[ii][0];
      py = PSP_pixel[ii][1];
      add_toptext(102,px,py,PSP_pixlab[ii],"Sans 8");
   }

   LMclick = RMclick = 0;
   Fpaint2();
   return;
}


//  Keyboard function
//  KB arrow keys tweak position of last touched tetragon corner.

void perspective::KBfunc(int key)
{
   using namespace perspective;

   int      ii, xstep, ystep;

   xstep = ystep = 0;
   if (key == GDK_KEY_Left) xstep = -1;
   if (key == GDK_KEY_Right) xstep = +1;
   if (key == GDK_KEY_Up) ystep = -1;
   if (key == GDK_KEY_Down) ystep = +1;

   ii = PSP_corner;
   if (ii < 0 || ii > 3) return;                                                 //  last corner touched, 0-3

   PSP_pixel[ii][0] += xstep;
   PSP_pixel[ii][1] += ystep;

   mousefunc();
   return;
}


//  perspective warp function - make input tetragon into a rectangle

void perspective::warpfunc()
{
   using namespace perspective;

   int         ii, jj, tempx, tempy, vstat;
   float       px3, py3, trpx[4], trpy[4];
   float       sqpx0, sqpy0, sqpx1, sqpy1, sqpx2, sqpy2, sqpx3, sqpy3;
   float       cdx0, cdy0, cdx1, cdy1, cdx2, cdy2, cdx3, cdy3;
   float       px1, py1, dispx, dispy, sqww, sqhh;
   float       f0, f1, f2, f3;
   float       vpix1[4], *pix3;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   if (PSP_npix != 4) {
      zmessageACK(Mwin,E2X("must have 4 corners"));
      return;
   }

   for (ii = 0; ii < 4; ii++) {                                                  //  get 4 selected tetragon points
      trpx[ii] = PSP_pixel[ii][0];
      trpy[ii] = PSP_pixel[ii][1];
   }

   //  sort 4 points in clockwise order NW, NE, SE, SW

   for (ii = 0; ii < 4; ii++) {                                                  //  sort top to bottom (y order)
      for (jj = ii; jj < 4; jj++) {
         if (trpy[jj] < trpy[ii]) {
            tempx = trpx[ii];
            tempy = trpy[ii];
            trpx[ii] = trpx[jj];
            trpy[ii] = trpy[jj];
            trpx[jj] = tempx;
            trpy[jj] = tempy;
         }
      }
   }

   if (trpx[1] < trpx[0]) {                                                      //  sort upper two left, right
      tempx = trpx[0];
      tempy = trpy[0];
      trpx[0] = trpx[1];
      trpy[0] = trpy[1];
      trpx[1] = tempx;
      trpy[1] = tempy;
   }

   if (trpx[2] < trpx[3]) {                                                      //  sort lower two right, left
      tempx = trpx[2];
      tempy = trpy[2];
      trpx[2] = trpx[3];
      trpy[2] = trpy[3];
      trpx[3] = tempx;
      trpy[3] = tempy;
   }

   if (trpx[0] < trpx[3]) sqpx0 = sqpx3 = trpx[0];                               //  rectangle enclosing tetragon
   else  sqpx0 = sqpx3 = trpx[3];
   if (trpx[1] > trpx[2]) sqpx1 = sqpx2 = trpx[1];
   else  sqpx1 = sqpx2 = trpx[2];
   if (trpy[0] < trpy[1]) sqpy0 = sqpy1 = trpy[0];
   else  sqpy0 = sqpy1 = trpy[1];
   if (trpy[2] > trpy[3]) sqpy2 = sqpy3 = trpy[2];
   else  sqpy2 = sqpy3 = trpy[3];

/***
   sqpx0 = sqpx3 = 0.5 * (trpx[0] + trpx[3]);                                    //  rectangle bisecting tetragon sides
   sqpx1 = sqpx2 = 0.5 * (trpx[1] + trpx[2]);
   sqpy0 = sqpy1 = 0.5 * (trpy[0] + trpy[1]);
   sqpy2 = sqpy3 = 0.5 * (trpy[2] + trpy[3]);
***/

   cdx0 = sqpx0 - trpx[0];                                                       //  displavement of tetragon corner
   cdy0 = sqpy0 - trpy[0];                                                       //    to corresponding rectangle corner
   cdx1 = sqpx1 - trpx[1];
   cdy1 = sqpy1 - trpy[1];
   cdx2 = sqpx2 - trpx[2];
   cdy2 = sqpy2 - trpy[2];
   cdx3 = sqpx3 - trpx[3];
   cdy3 = sqpy3 - trpy[3];

   sqww = 1.0 / (sqpx1 - sqpx0);                                                 //  rectangle width and height
   sqhh = 1.0 / (sqpy3 - sqpy0);

   for (py3 = 0; py3 < E3pxm->hh; py3++)                                         //  loop all output pixels
   for (px3 = 0; px3 < E3pxm->ww; px3++)
   {
      f0 = (1.0 - (px3 - sqpx0) * sqww) * (1.0 - (py3 - sqpy0) * sqhh);
      f1 = (px3 - sqpx0) * sqww * (1.0 - (py3 - sqpy0) * sqhh);
      f2 = (px3 - sqpx0) * sqww * (py3 - sqpy0) * sqhh;
      f3 = (1.0 - (px3 - sqpx0) * sqww) * (py3 - sqpy0) * sqhh;

      dispx = cdx0 * f0 + cdx1 * f1 + cdx2 * f2 + cdx3 * f3;
      dispy = cdy0 * f0 + cdy1 * f1 + cdy2 * f2 + cdy3 * f3;

      px1 = px3 - dispx;                                                         //  input virtual pixel for px3/py3
      py1 = py3 - dispy;

      pix3 = PXMpix(E3pxm,int(px3),int(py3));                                    //  output pixel

      vstat = vpixel(E1pxm,px1,py1,vpix1);                                       //  output pixel = input virtual pixel
      if (vstat) memcpy(pix3,vpix1,pcc);
      else memset(pix3,0,pcc);                                                   //  voided pixel
   }

   CEF->Fmods++;                                                                 //  image is modified
   CEF->Fsaved = 0;

   Fpaint2();                                                                    //  update window
   return;
}


/********************************************************************************/

//  warp/distort area - select image area and pull with mouse

float       *WarpAx, *WarpAy;                                                    //  memory for pixel warp vectors
int         WarpAcc, WarpAnew;
int         WarpA_started;
int         WarpA_areanumber;

editfunc    EFwarpA;

void  WarpA_init();
void  WarpA_warpfunc(float mdx, float mdy, float mdw, float mdh, int acc);
void  WarpA_warpfunc2(float mdx, float mdy, float mdw, float mdh, int acc);
void  WarpA_mousefunc();
void  WarpA_expand();
void  WarpA_edgeblend();


//  menu function

void m_warp_area(GtkWidget *, cchar *)
{
   int      WarpA_dialog_event(zdialog *zd, cchar *event);

   cchar  *WarpA_message = E2X(
             " Select an area to warp using select area function. \n"
             " Press [start warp] and pull area with mouse. \n"
             " Make multiple mouse pulls until satisfied. \n"
             " When finished, select another area or press [done].");

   F1_help_topic = "warp area";

   EFwarpA.menufunc = m_warp_area;
   EFwarpA.funcname = "warp_area";
   EFwarpA.Farea = 2;                                                            //  select area usable
   EFwarpA.mousefunc = WarpA_mousefunc;                                          //  mouse function
   if (! edit_setup(EFwarpA)) return;                                            //  setup edit

   zdialog *zd = zdialog_new(E2X("Warp area"),Mwin,Bdone,Bcancel,null);
   EFwarpA.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",WarpA_message,"space=3");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","start","hb1",E2X("start warp"),"space=5");
   zdialog_add_widget(zd,"button","reset","hb1",Breset,"space=5");

   WarpAcc = E3pxm->ww * E3pxm->hh * sizeof(float);
   WarpAx = (float *) zmalloc(WarpAcc);                                          //  get memory for pixel warp vectors
   WarpAy = (float *) zmalloc(WarpAcc);
   memset(WarpAx,0,WarpAcc);
   memset(WarpAy,0,WarpAcc);

   WarpA_started = 0;
   WarpA_areanumber = 0;
   WarpAnew = 0;

   zdialog_run(zd,WarpA_dialog_event,"save");                                    //  run dialog, parallel
   return;
}


//  dialog event and completion callback function

int WarpA_dialog_event(zdialog * zd, cchar *event)
{
   int      init = 0;

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()

   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit

      zfree(WarpAx);                                                             //  release memory
      zfree(WarpAy);
      return 1;
   }

   if (! sa_validate()) init = 1;                                                //  area invalid for curr. image file

   if (sa_stat != 3 || sa_mode == mode_image)                                    //  no select area active
      init = 1;

   if (WarpA_started && WarpA_areanumber != areanumber)                          //  select area changed
      init = 1;

   if (init) {
      memset(WarpAx,0,WarpAcc);
      memset(WarpAy,0,WarpAcc);
      WarpA_started = 0;
      WarpA_areanumber = 0;
      return 1;
   }

   if (strmatch(event,"start"))                                                  //  start warp
   {
      if (sa_stat != 3 || sa_mode == mode_image) {                               //  no select area active
         zmessageACK(Mwin,E2X("no active Select Area"));
         return 1;
      }
      sa_edgecalc();                                                             //  calculate area edge distances
      takeMouse(WarpA_mousefunc,dragcursor);                                     //  connect mouse function
      WarpA_started = 1;
      WarpA_areanumber = areanumber;
   }

   if (strmatch(event,"focus"))                                                  //  toggle mouse capture
      takeMouse(WarpA_mousefunc,dragcursor);

   if (strmatch(event,"reset")) {                                                //  undo all warps
      edit_reset();
      memset(WarpAx,0,WarpAcc);                                                  //  clear warp vectors
      memset(WarpAy,0,WarpAcc);
   }

   if (strmatch(event,"blendwidth"))
      WarpA_edgeblend();

   return 1;
}


//  warp mouse function

void  WarpA_mousefunc()
{
   static float   mdx, mdy, mdw, mdh;
   static int     warped = 0;

   if (! WarpA_started) return;

   if (Mxdrag || Mydrag)                                                         //  mouse drag underway
   {
      mdx = Mxdown;                                                              //  drag origin, image coordinates
      mdy = Mydown;
   // int ii = mdy * Fpxb->ww + mdx;                                             //  disable drag outside area
   // if (! sa_pixmap[ii]) return;
      mdw = Mxdrag - Mxdown;                                                     //  drag increment
      mdh = Mydrag - Mydown;
      WarpA_warpfunc(mdx,mdy,mdw,mdh,0);                                         //  warp image
      warped = 1;
      Mxdrag = Mydrag = 0;
      return;
   }

   else if (warped)
   {
      warped = 0;
      WarpA_warpfunc(mdx,mdy,mdw,mdh,1);                                         //  drag done, update warp vectors
      if (WarpAnew) WarpA_expand();
   }

   return;
}


//  warp image and accumulate warp memory
//  mdx/y = mouse initial position
//  mdw/h = mouse drag vector

void WarpA_warpfunc(float mdx, float mdy, float mdw, float mdh, int acc)
{
   int         ii, px, py, ww, hh, vstat;
   float       ddx, ddy, dpe, dpm1, dpm2, dpm;
   float       mag, dispx, dispy;
   float       vpix[4], *pix3;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   if (sa_stat != 3) return;                                                     //  area erased

   ww = E1pxm->ww;
   hh = E1pxm->hh;

   ii = mdy * ww + mdx;
   if (! sa_pixmap[ii]) {                                                        //  if mouse drag outside select area
      WarpA_warpfunc2(mdx,mdy,mdw,mdh,acc);                                      //    then use alternate function
      return;
   }

   for (py = sa_miny; py < sa_maxy; py++)                                        //  loop all pixels in area
   for (px = sa_minx; px < sa_maxx; px++)
   {
      ii = py * ww + px;
      dpe = sa_pixmap[ii];                                                       //  distance from area edge
      if (dpe < 1) continue;                                                     //  outside area
      dpe -= 1;                                                                  //  rebase edge = 0

      ddx = (px - mdx);                                                          //  pixel distance to mouse position
      ddy = (py - mdy);                                                          //    before drag
      dpm1 = sqrt(ddx*ddx + ddy*ddy);

      ddx -= mdw;                                                                //  pixel distance to mouse position
      ddy -= mdh;                                                                //    after drag
      dpm2 = sqrt(ddx*ddx + ddy*ddy);

      dpm = 0.5 * (dpm1 + dpm2);                                                 //  mean

      mag = (dpe / (dpm + dpe));                                                 //  1...0 for pixel at mouse...edge

      dispx = -mdw * mag;                                                        //  pixel movement from drag movement
      dispy = -mdh * mag;

      dispx += WarpAx[ii];                                                       //  add this warp to prior
      dispy += WarpAy[ii];

      if (acc) {                                                                 //  mouse drag done,
         WarpAx[ii] = dispx;                                                     //    accumulate warp memory
         WarpAy[ii] = dispy;
         continue;
      }

      vstat = vpixel(E1pxm,px+dispx,py+dispy,vpix);                              //  input virtual pixel
      if (vstat) {
         pix3 = PXMpix(E3pxm,px,py);                                             //  output pixel
         memcpy(pix3,vpix,pcc);
      }
   }

   ww = sa_maxx - sa_minx;                                                       //  update window
   hh = sa_maxy - sa_miny;
   Fpaint3(sa_minx,sa_miny,ww,hh,0);

   CEF->Fmods++;
   CEF->Fsaved = 0;
   return;
}


//  warp function when mouse is dragged outside select area

void  WarpA_warpfunc2(float mdx, float mdy, float mdw, float mdh, int acc)
{
   int         ii, jj, px, py, dpx, dpy, dpe;
   int         xlo, xhi, ylo, yhi;
   int         ww, hh, vstat;
   float       ddx, ddy, dpm1, dpm2, dpm;
   float       mag, dispx, dispy;
   float       mdmax, mdmax2;
   float       vpix[4], *pix1, *pix3;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   #define distance(px1,py1,px2,py2) \
            sqrtf((px2-px1)*(px2-px1)+(py2-py1)*(py2-py1))

   WarpAnew = 1;                                                                 //  note select area will expand

   ww = E1pxm->ww;
   hh = E1pxm->hh;

   ylo = sa_miny;                                                                //  select area enclosing rectangle
   yhi = sa_maxy;
   xlo = sa_minx;
   xhi = sa_maxx;

   if (mdy < ylo) ylo = mdy - 200;                                               //  extend area to mouse posn + more
   if (mdy > yhi) yhi = mdy + 200;
   if (mdx < xlo) xlo = mdx - 200;
   if (mdx > xhi) xhi = mdx + 200;

   if (ylo < 0) ylo = 0;
   if (yhi > hh) yhi = hh;
   if (xlo < 0) xlo = 0;
   if (xhi > ww) xhi = ww;

   mdmax = distance(mdx,mdy,xlo,ylo);                                            //  find max. mouse distance from
   mdmax2 = distance(mdx,mdy,xhi,ylo);                                           //    enclosing rectangle corners
   if (mdmax2 > mdmax) mdmax = mdmax2;
   mdmax2 = distance(mdx,mdy,xhi,yhi);
   if (mdmax2 > mdmax) mdmax = mdmax2;
   mdmax2 = distance(mdx,mdy,xlo,yhi);
   if (mdmax2 > mdmax) mdmax = mdmax2;

   for (py = ylo; py < yhi; py++)                                                //  loop all pixels in enclosing rectangle
   for (px = xlo; px < xhi; px++)
   {
      ddx = (px - mdx);                                                          //  pixel distance to mouse initial position
      ddy = (py - mdy);
      dpm1 = sqrt(ddx*ddx + ddy*ddy);

      ddx -= mdw;                                                                //  pixel distance to mouse curr. position
      ddy -= mdh;
      dpm2 = sqrt(ddx*ddx + ddy*ddy);

      dpm = 0.5 * (dpm1 + dpm2);                                                 //  mean

      mag = 1.0 - dpm / mdmax;                                                   //  1...0 for pixel-mouse 0...mdmax

      dispx = -mdw * mag;
      dispy = -mdh * mag;                                                        //  pixel movement from drag movement

      ii = py * ww + px;
      dispx += WarpAx[ii];                                                       //  add this warp to prior
      dispy += WarpAy[ii];

      dpx = px + dispx;                                                          //  source virtual pixel
      dpy = py + dispy;                                                          //  (nearest real pixel)
      if (dpx < 0) dpx = 0;
      if (dpx > ww-1) dpx = ww-1;
      if (dpy < 0) dpy = 0;
      if (dpy > hh-1) dpy = hh-1;
      jj = dpy * ww + dpx;
      dpe = sa_pixmap[jj];                                                       //  distance from area edge

      if (dpe < 1)                                                               //  outside area
         dispx = dispy = 0;

      if (acc) {                                                                 //  mouse drag done,
         WarpAx[ii] = dispx;                                                     //    accumulate warp memory
         WarpAy[ii] = dispy;
         continue;
      }

      if (dpe) {                                                                 //  source pixel inside area
         vstat = vpixel(E1pxm,px+dispx,py+dispy,vpix);                           //   = input virtual pixel
         if (vstat) {
            pix3 = PXMpix(E3pxm,px,py);                                          //  output pixel
            memcpy(pix3,vpix,pcc);
            continue;
         }
      }

      pix1 = PXMpix(E1pxm,px,py);                                                //  pixel is unchanged
      pix3 = PXMpix(E3pxm,px,py);
      memcpy(pix3,pix1,pcc); 
   }

   ww = xhi - xlo;                                                               //  update window
   hh = yhi - ylo;
   Fpaint3(xlo,ylo,ww,hh,0);

   CEF->Fmods++;
   CEF->Fsaved = 0;
   return;
}


//  expand select area if pulled outside the original bounds

void  WarpA_expand()
{
   int      ii, px, py, ww, hh;
   int      blend = sa_blendwidth;

   ww = E1pxm->ww;
   hh = E1pxm->hh;

   for (py = 0; py < hh; py++)                                                   //  loop all pixels
   for (px = 0; px < ww; px++)
   {
      ii = py * ww + px;                                                         //  find pixels that have moved
      if (! WarpAx[ii] && ! WarpAy[ii]) continue;
      sa_pixmap[ii] = 1;                                                         //  mark within area
   }

   sa_map_pixels();
   sa_finish_auto();
   sa_edgecalc();                                                                //  recalculate edge distances

   sa_blendwidth = blend;
   WarpA_edgeblend();

   WarpA_areanumber = areanumber;
   WarpAnew = 0;

   return;
}


//  blend area edges according to sa_blendwidth

void  WarpA_edgeblend()
{
   int      ii, px, py, ww, hh, dist, vstat;
   float    *pix1, *pix3, vpix[4];
   float    dold, dnew, dispx, dispy;

   if (! sa_blendwidth) return;

   ww = E1pxm->ww;
   hh = E1pxm->hh;

   for (py = 0; py < hh; py++)                                                   //  loop output pixels
   for (px = 0; px < ww; px++)
   {
      ii = py * ww + px;                                                         //  blend changes at edge
      dist = sa_pixmap[ii];
      if (! dist) continue;                                                      //  outside area
      if (dist > sa_blendwidth) continue;                                        //  beyond blend area

      dispx = WarpAx[ii];                                                        //  warp
      dispy = WarpAy[ii];
      vstat = vpixel(E1pxm,px+dispx,py+dispy,vpix);                              //  input virtual pixel (new)
      if (! vstat) continue;
      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel (old)
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      dnew = sa_blendfunc(dist);
      dold = 1.0 - dnew;
      pix3[0] = dnew * vpix[0] + dold * pix1[0];
      pix3[1] = dnew * vpix[1] + dold * pix1[1];
      pix3[2] = dnew * vpix[2] + dold * pix1[2];
   }

   ww = sa_maxx - sa_minx;                                                       //  update window
   hh = sa_maxy - sa_miny;
   Fpaint3(sa_minx,sa_miny,ww,hh,0);

   return;
}


/********************************************************************************/

//  warp/distort whole image with a curved transform
//  fix perspective problems (e.g. curved walls, leaning buildings)

namespace warpC_names
{
   float       *WarpCx, *WarpCy;                                                 //  memory of all dragged pixels
   float       WarpCmem[5][100];                                                 //  undo memory, last 100 drags
   int         NWarpC;                                                           //  WarpCmem count
   int         WarpCdrag;
   int         E3ww, E3hh;

   float       $mdx, $mdy, $mdw, $mdh;                                           //  warpC_warpfunc() and warpC_wthread()
   float       $D, $span;                                                        //    use these $ args
   int         $acc;

   editfunc    EFwarpC;

   int    WarpC_dialog_event(zdialog *zd, cchar *event);
   void   WarpC_warpfunc();
   void   WarpC_mousefunc(void);
   void * WarpC_wthread(void *arg);
}

//  menu function

void m_warp_curved(GtkWidget *, cchar *)
{
   using namespace warpC_names;

   cchar  *WarpC_message = E2X(
             " Pull an image position using the mouse. \n"
             " Make multiple mouse pulls until satisfied. \n"
             " When finished, press [done].");

   int         px, py, ii;

   F1_help_topic = "warp curved";

   EFwarpC.menufunc = m_warp_curved;
   EFwarpC.funcname = "warp_curved";
   EFwarpC.FprevReq = 1;                                                         //  use preview
   EFwarpC.mousefunc = WarpC_mousefunc;                                          //  mouse function
   if (! edit_setup(EFwarpC)) return;                                            //  setup edit

   PXM_addalpha(E0pxm);
   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

   zdialog *zd = zdialog_new(E2X("Warp curved"),Mwin,Bdone,Bcancel,null);
   EFwarpC.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",WarpC_message,"space=3");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=8");
   zdialog_add_widget(zd,"button","undolast","hb1",Bundolast,"space=8");
   zdialog_add_widget(zd,"button","undoall","hb1",Bundoall,"space=2");
   zdialog_add_widget(zd,"button","grid","hb1",Bgrid,"space=15");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=4");
   zdialog_add_widget(zd,"label","lab2","hb2",E2X("warp span"),"space=8");
   zdialog_add_widget(zd,"zspin","span","hb2","0.00|1.0|0.01|0.1","space=1");

   currgrid = 3;                                                                 //  use warp_curved grid

   NWarpC = WarpCdrag = 0;                                                       //  no drag data

   int cc = E3pxm->ww * E3pxm->hh * sizeof(float);
   WarpCx = (float *) zmalloc(cc);                                               //  get memory for pixel displacements
   WarpCy = (float *) zmalloc(cc);

   for (py = 0; py < E3pxm->hh; py++)                                            //  no pixel displacements
   for (px = 0; px < E3pxm->ww; px++)
   {
      ii = py * E3pxm->ww + px;
      WarpCx[ii] = WarpCy[ii] = 0.0;
   }

   E3ww = E3pxm->ww;                                                             //  preview dimensions
   E3hh = E3pxm->hh;

   zdialog_restore_inputs(zd);                                                   //  restore previous inputs
   zdialog_fetch(zd,"span",$span);                                               //  save span value

   zdialog_run(zd,WarpC_dialog_event,"save");                                    //  run dialog, parallel
   takeMouse(WarpC_mousefunc,dragcursor);                                        //  connect mouse function
   return;
}


//  dialog event and completion callback function

int warpC_names::WarpC_dialog_event(zdialog * zd, cchar *event)
{
   using namespace warpC_names;

   int         px, py, ii;
   int         fpx, fpy, epx, epy, vstat;
   float       scale, dispx, dispy;
   float       vpix[4], *pix3;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()

   if (zd->zstat) goto complete;

   if (strmatch(event,"undolast"))
   {
      if (NWarpC == 1) event = "undoall";
      else if (NWarpC) {                                                         //  undo most recent drag
         ii = --NWarpC;
         $mdx = WarpCmem[0][ii];
         $mdy = WarpCmem[1][ii];
         $mdw = -WarpCmem[2][ii];
         $mdh = -WarpCmem[3][ii];
         $span = WarpCmem[4][ii];
         zdialog_stuff(zd,"span",$span);
         $acc = 0;
         WarpC_warpfunc();                                                       //  undrag image
         $acc = 1;
         WarpC_warpfunc();                                                       //  undrag memory
      }
   }

   if (strmatch(event,"undoall"))                                                //  undo all drags
   {
      NWarpC = 0;                                                                //  erase undo memory

      for (py = 0; py < E3pxm->hh; py++)                                         //  reset pixel displacements
      for (px = 0; px < E3pxm->ww; px++)
      {
         ii = py * E3pxm->ww + px;
         WarpCx[ii] = WarpCy[ii] = 0.0;
      }
      edit_reset();                                                              //  restore image 1
   }

   if (strmatch(event,"grid")) m_gridlines(0,"grid 3");                          //  grid settings dialog

   if (strmatch(event,"span"))
      zdialog_fetch(zd,"span",$span);

   return 1;

complete:

   currgrid = 0;                                                                 //  restore normal grid settings

   if (zd->zstat)
   {
      if (NWarpC == 0) zd->zstat = 2;                                            //  no warps, cancel

      if (zd->zstat == 1)                                                        //  done
      {
         edit_fullsize();                                                        //  get full size image

         scale = 1.0 * (E3pxm->ww + E3pxm->hh) / (E3ww + E3hh);

         for (fpy = 0; fpy < E3pxm->hh; fpy++)                                   //  scale net pixel displacements
         for (fpx = 0; fpx < E3pxm->ww; fpx++)                                   //    to full image size
         {
            epx = E3ww * fpx / E3pxm->ww;
            epy = E3hh * fpy / E3pxm->hh;
            ii = epy * E3ww + epx;
            dispx = WarpCx[ii] * scale;
            dispy = WarpCy[ii] * scale;
            vstat = vpixel(E1pxm,fpx+dispx,fpy+dispy,vpix);                      //  input virtual pixel
            pix3 = PXMpix(E3pxm,fpx,fpy);                                        //  output pixel
            if (vstat) memcpy(pix3,vpix,pcc);
            else memset(pix3,0,pcc);                                             //  voided pixel
         }
         signal_thread();
         edit_done(0);                                                           //  commit edit
      }

      else edit_cancel(0);                                                       //  discard edit
      zfree(WarpCx);                                                             //  release memory
      zfree(WarpCy);
   }

   return 1;
}


//  WarpC mouse function

void warpC_names::WarpC_mousefunc()
{
   using namespace warpC_names;

   int      ii;

   if (Mxdrag || Mydrag)                                                         //  mouse drag underway
   {
      $mdx = Mxdown;                                                             //  drag origin
      $mdy = Mydown;
      $mdw = Mxdrag - Mxdown;                                                    //  drag increment
      $mdh = Mydrag - Mydown;
      $acc = 0;
      WarpC_warpfunc();                                                          //  drag image
      WarpCdrag = 1;
      Mxdrag = Mydrag = 0;
      return;
   }

   else if (WarpCdrag)
   {
      WarpCdrag = 0;
      $acc = 1;
      WarpC_warpfunc();                                                          //  drag done, add to memory

      if (NWarpC == 100)                                                         //  if full, throw away oldest
      {
         NWarpC = 99;
         for (ii = 0; ii < NWarpC; ii++)
         {
            WarpCmem[0][ii] = WarpCmem[0][ii+1];
            WarpCmem[1][ii] = WarpCmem[1][ii+1];
            WarpCmem[2][ii] = WarpCmem[2][ii+1];
            WarpCmem[3][ii] = WarpCmem[3][ii+1];
            WarpCmem[4][ii] = WarpCmem[4][ii+1];
         }
      }

      ii = NWarpC;
      WarpCmem[0][ii] = $mdx;                                                    //  save drag for undo
      WarpCmem[1][ii] = $mdy;
      WarpCmem[2][ii] = $mdw;
      WarpCmem[3][ii] = $mdh;
      WarpCmem[4][ii] = $span;
      NWarpC++;
   }

   return;
}


//  warp image and accumulate warp memory
//  mouse at (mx,my) is moved (mw,mh) pixels

void warpC_names::WarpC_warpfunc()
{
   using namespace warpC_names;

   float       D, d1, d2, d3, d4;

   d1 = ($mdx-0) * ($mdx-0) + ($mdy-0) * ($mdy-0);                               //  distance, mouse to 4 corners
   d2 = (E3pxm->ww-$mdx) * (E3pxm->ww-$mdx) + ($mdy-0) * ($mdy-0);
   d3 = (E3pxm->ww-$mdx) * (E3pxm->ww-$mdx) + (E3pxm->hh-$mdy) * (E3pxm->hh-$mdy);
   d4 = ($mdx-0) * ($mdx-0) + (E3pxm->hh-$mdy) * (E3pxm->hh-$mdy);

   D = d1;
   if (d2 > D) D = d2;                                                           //  find greatest corner distance
   if (d3 > D) D = d3;
   if (d4 > D) D = d4;

   $D = D * $span;

   do_wthreads(WarpC_wthread,NWT);                                               //  worker threads

   CEF->Fmods++;
   CEF->Fsaved = 0;
   Fpaint2();                                                                    //  update window
   return;
}


//  working thread to process the pixels

void * warpC_names::WarpC_wthread(void *arg)
{
   using namespace warpC_names;

   int      index = *((int *) arg);
   int      ii, px, py, vstat;
   float    d, mag, dispx, dispy;
   float    vpix[4], *pix3;
   int      nc = E1pxm->nc, pcc = nc * sizeof(float);

   for (py = index; py < E3pxm->hh; py += NWT)                                   //  process all pixels
   for (px = 0; px < E3pxm->ww; px++)
   {
      d = (px-$mdx)*(px-$mdx) + (py-$mdy)*(py-$mdy);
      mag = (1.0 - d / $D);
      if (mag < 0) continue;

      mag = mag * mag;                                                           //  faster than pow(mag,4);
      mag = mag * mag;

      dispx = -$mdw * mag;                                                       //  displacement = drag * mag
      dispy = -$mdh * mag;

      ii = py * E3pxm->ww + px;

      if ($acc) {                                                                //  drag done, accumulate drag sum
         WarpCx[ii] += dispx;
         WarpCy[ii] += dispy;
         continue;
      }

      dispx += WarpCx[ii];                                                       //  add this drag to prior sum
      dispy += WarpCy[ii];

      vstat = vpixel(E1pxm,px+dispx,py+dispy,vpix);                              //  input virtual pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      if (vstat) memcpy(pix3,vpix,pcc);
      else memset(pix3,0,pcc);                                                   //  voided pixel
   }

   pthread_exit(0);                                                              //  exit thread
}


/********************************************************************************/

//  warp/distort whole image with a linear transform (almost)
//  fix perspective problems (e.g. leaning buildings)

namespace warpL_names
{
   float       *WarpLx, *WarpLy;                                                 //  memory of all dragged pixels
   float       WarpLmem[4][100];                                                 //  undo memory, last 100 drags
   int         NWarpL;                                                           //  WarpLmem count
   int         WarpLdrag;
   int         E3ww, E3hh;
   float       $mdx, $mdy, $mdw, $mdh;                                           //  warpL_warpfunc() and warpL_wthread()
   int         $D, $Dx, $Dy, $acc;                                               //    use these $ args

   editfunc    EFwarpL;

   int    WarpL_dialog_event(zdialog *zd, cchar *event);
   void   WarpL_mousefunc(void);
   void   WarpL_warpfunc();
   void * WarpL_wthread(void *arg);
}


//  menu function

void m_warp_linear(GtkWidget *, cchar *)
{
   using namespace warpL_names;

   cchar  *WarpL_message = E2X(
             " Pull an image position using the mouse. \n"
             " Make multiple mouse pulls until satisfied. \n"
             " When finished, press [done].");

   int         px, py, ii;

   F1_help_topic = "warp linear";

   EFwarpL.menufunc = m_warp_linear;
   EFwarpL.funcname = "warp_linear";
   EFwarpL.FprevReq = 1;                                                         //  use preview
   EFwarpL.mousefunc = WarpL_mousefunc;                                          //  mouse function
   if (! edit_setup(EFwarpL)) return;                                            //  setup edit

   PXM_addalpha(E0pxm);
   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

   zdialog *zd = zdialog_new(E2X("Warp linear"),Mwin,Bdone,Bcancel,null);
   EFwarpL.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",WarpL_message,"space=3");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10");
   zdialog_add_widget(zd,"button","undolast","hb1",Bundolast,"space=5");
   zdialog_add_widget(zd,"button","undoall","hb1",Bundoall,"space=5");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=6");
   zdialog_add_widget(zd,"button","grid","hb2",Bgrid,"space=10");

   currgrid = 4;                                                                 //  use warp_linear grid

   NWarpL = WarpLdrag = 0;                                                       //  no drag data

   int cc = E3pxm->ww * E3pxm->hh * sizeof(float);
   WarpLx = (float *) zmalloc(cc);                                               //  get memory for pixel displacements
   WarpLy = (float *) zmalloc(cc);

   for (py = 0; py < E3pxm->hh; py++)                                            //  no pixel displacements
   for (px = 0; px < E3pxm->ww; px++)
   {
      ii = py * E3pxm->ww + px;
      WarpLx[ii] = WarpLy[ii] = 0.0;
   }

   E3ww = E3pxm->ww;                                                             //  preview dimensions
   E3hh = E3pxm->hh;

   zdialog_run(zd,WarpL_dialog_event,"save");                                    //  run dialog, parallel

   takeMouse(WarpL_mousefunc,dragcursor);                                        //  connect mouse function
   return;
}


//  dialog event and completion callback function

int warpL_names::WarpL_dialog_event(zdialog * zd, cchar *event)
{
   using namespace warpL_names;

   int         px, py, ii;
   int         fpx, fpy, epx, epy, vstat;
   float       scale, dispx, dispy;
   float       vpix[4], *pix3;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()

   if (zd->zstat) goto complete;

   if (strmatch(event,"undolast"))
   {
      if (NWarpL == 1) event = "undoall";
      else if (NWarpL) {                                                         //  undo most recent drag
         ii = --NWarpL;
         $mdx = WarpLmem[0][ii];
         $mdy = WarpLmem[1][ii];
         $mdw = -WarpLmem[2][ii];
         $mdh = -WarpLmem[3][ii];
         $acc = 0;
         WarpL_warpfunc();                                                       //  undrag image
         $acc = 1;
         WarpL_warpfunc();                                                       //  undrag memory
      }
   }

   if (strmatch(event,"undoall"))                                                //  undo all drags
   {
      NWarpL = 0;                                                                //  erase undo memory

      for (py = 0; py < E3pxm->hh; py++)                                         //  reset pixel displacements
      for (px = 0; px < E3pxm->ww; px++)
      {
         ii = py * E3pxm->ww + px;
         WarpLx[ii] = WarpLy[ii] = 0.0;
      }
      edit_reset();                                                              //  restore image 1
   }

   if (strmatch(event,"grid")) m_gridlines(0,"grid 4");                          //  grid settings dialog

   return 1;

complete:

   currgrid = 0;                                                                 //  restore normal grid settings

   if (zd->zstat != 1 || NWarpL == 0)                                            //  cancel or no warps made
      edit_cancel(0);

   else
   {
      edit_fullsize();                                                           //  get full-size E1/E3

      scale = 1.0 * (E3pxm->ww + E3pxm->hh) / (E3ww + E3hh);

      for (fpy = 0; fpy < E3pxm->hh; fpy++)                                      //  scale net pixel displacements
      for (fpx = 0; fpx < E3pxm->ww; fpx++)                                      //    to full image size
      {
         epx = E3ww * fpx / E3pxm->ww;
         epy = E3hh * fpy / E3pxm->hh;
         ii = epy * E3ww + epx;
         dispx = WarpLx[ii] * scale;
         dispy = WarpLy[ii] * scale;

         vstat = vpixel(E1pxm,fpx+dispx,fpy+dispy,vpix);                         //  input virtual pixel
         pix3 = PXMpix(E3pxm,fpx,fpy);                                           //  output pixel
         if (vstat) memcpy(pix3,vpix,pcc);
         else memset(pix3,0,pcc);                                                //  voided pixel
      }

      signal_thread();
      edit_done(0);
   }

   zfree(WarpLx);                                                                //  release memory
   zfree(WarpLy);
   return 1;
}


//  WarpL mouse function

void warpL_names::WarpL_mousefunc()
{
   using namespace warpL_names;

   int      ii;

   if (Mxdrag || Mydrag)                                                         //  mouse drag underway
   {
      $mdx = Mxdown;                                                             //  drag origin, window coordinates
      $mdy = Mydown;
      $mdw = Mxdrag - Mxdown;                                                    //  drag increment
      $mdh = Mydrag - Mydown;
      $acc = 0;
      WarpL_warpfunc();                                                          //  drag image
      WarpLdrag = 1;
      Mxdrag = Mydrag = 0;
      return;
   }

   else if (WarpLdrag)
   {
      WarpLdrag = 0;
      $acc = 1;
      WarpL_warpfunc();                                                          //  drag done, add to memory

      if (NWarpL == 100)                                                         //  if full, throw away oldest
      {
         NWarpL = 99;
         for (ii = 0; ii < NWarpL; ii++)
         {
            WarpLmem[0][ii] = WarpLmem[0][ii+1];
            WarpLmem[1][ii] = WarpLmem[1][ii+1];
            WarpLmem[2][ii] = WarpLmem[2][ii+1];
            WarpLmem[3][ii] = WarpLmem[3][ii+1];
         }
      }

      ii = NWarpL;
      WarpLmem[0][ii] = $mdx;                                                    //  save drag for undo
      WarpLmem[1][ii] = $mdy;
      WarpLmem[2][ii] = $mdw;
      WarpLmem[3][ii] = $mdh;
      NWarpL++;
   }

   return;
}


//  warp image and accumulate warp memory
//  mouse at ($mdx,$mdy) is moved ($mdw,$mdh) pixels

void warpL_names::WarpL_warpfunc()
{
   using namespace warpL_names;

   float       d1, d2, d3, d4;

   d1 = ($mdx-0) * ($mdx-0) + ($mdy-0) * ($mdy-0);                               //  distance, mouse to 4 corners
   d2 = (E3pxm->ww-$mdx) * (E3pxm->ww-$mdx) + ($mdy-0) * ($mdy-0);
   d3 = (E3pxm->ww-$mdx) * (E3pxm->ww-$mdx) + (E3pxm->hh-$mdy) * (E3pxm->hh-$mdy);
   d4 = ($mdx-0) * ($mdx-0) + (E3pxm->hh-$mdy) * (E3pxm->hh-$mdy);

   $D = d1;
   if (d2 > $D) $D = d2;                                                         //  find greatest corner distance
   if (d3 > $D) $D = d3;
   if (d4 > $D) $D = d4;

   if ($D == d1) {                                                               //  NW corner
      $D = 1;
      $Dx = $mdx;                                                                //  x/y distance, mouse to edges
      $Dy = $mdy;
   }

   if ($D == d2) {                                                               //  NE
      $D = 2;
      $Dx = E3ww - $mdx;
      $Dy = $mdy;
   }

   if ($D == d3) {                                                               //  SE
      $D = 3;
      $Dx = E3ww - $mdx;
      $Dy = E3hh - $mdy;
   }

   if ($D == d4) {                                                               //  SW
      $D = 4;
      $Dx = $mdx;
      $Dy = E3hh - $mdy;
   }

   do_wthreads(WarpL_wthread,NWT);                                               //  worker threads

   CEF->Fmods++;
   CEF->Fsaved = 0;
   Fpaint2();                                                                    //  update window
   return;
}


//  working thread to process the pixels

void * warpL_names::WarpL_wthread(void *arg)
{
   using namespace warpL_names;

   int      index = *((int *) arg);
   int      ii, px, py, vstat;
   float    dx, dy, mag, dispx, dispy;
   float    vpix[4], *pix3;
   int      nc = E1pxm->nc, pcc = nc * sizeof(float);

   for (py = index; py < E3pxm->hh; py += NWT)                                   //  process all pixels
   for (px = 0; px < E3pxm->ww; px++)
   {
      if ($D == 1) {
         dx = $mdx - px;                                                         //  x/y distance, pixel to mouse
         dy = $mdy - py;
      }

      else if ($D == 2) {
         dx = px - $mdx;
         dy = $mdy - py;
      }

      else if ($D == 3) {
         dx = px - $mdx;
         dy = py - $mdy;
      }

      else /* $D == 4 */ {
         dx = $mdx - px;
         dy = py - $mdy;
      }

      mag = (1.0 - dx / $Dx) * (1.0 - dy / $Dy);                                 //  pixel movement / mouse drag

      dispx = -$mdw * mag;                                                       //  displacement = drag * mag
      dispy = -$mdh * mag;

      ii = py * E3pxm->ww + px;

      if ($acc) {                                                                //  drag done, accumulate drag sum
         WarpLx[ii] += dispx;
         WarpLy[ii] += dispy;
         continue;
      }

      dispx += WarpLx[ii];                                                       //  add this drag to prior sum
      dispy += WarpLy[ii];

      vstat = vpixel(E1pxm,px+dispx,py+dispy,vpix);                              //  input virtual pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      if (vstat) memcpy(pix3,vpix,pcc);
      else memset(pix3,0,pcc);                                                   //  voided pixel
   }

   pthread_exit(0);                                                              //  exit thread
}


/********************************************************************************/

//  warp/distort whole image using affine transform
//  (straight lines remain straight)

float       WarpF_old[3][2];                                                     //  3 original image points
float       WarpF_new[3][2];                                                     //  corresponding warped points
float       WarpF_coeff[6];                                                      //  transform coefficients
float       WarpF_Icoeff[6];                                                     //  inverse transform coefficients
int         WarpF_ftf;                                                           //  first time flag

editfunc    EFwarpF;

void  WarpF_warpfunc();                                                          //  image warp function
void  WarpF_mousefunc(void);
void  WarpF_affine(float po[3][2], float pn[3][2], float coeff[6]);              //  compute affine transform coefficients
void  WarpF_invert(float coeff[6], float Icoeff[6]);                             //  compute reverse transform coefficients


//  menu function

void m_warp_affine(GtkWidget *, cchar *)
{
   int      WarpF_dialog_event(zdialog *zd, cchar *event);

   cchar  *WarpF_message = E2X(
             " Pull on an image corner using the mouse. \n"
             " Make multiple mouse pulls until satisfied. \n"
             " When finished, press [done].");

   F1_help_topic = "warp affine";

   EFwarpF.menufunc = m_warp_affine;
   EFwarpF.funcname = "warp_affine";
   EFwarpF.FprevReq = 1;                                                         //  use preview
   EFwarpF.mousefunc = WarpF_mousefunc;                                          //  mouse function
   if (! edit_setup(EFwarpF)) return;                                            //  setup edit

   PXM_addalpha(E0pxm);
   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

   zdialog *zd = zdialog_new(E2X("Warp affine"),Mwin,Bdone,Bcancel,null);
   EFwarpF.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",WarpF_message,"space=3");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=6");
   zdialog_add_widget(zd,"button","grid","hb2",Bgrid,"space=10");

   currgrid = 5;                                                                 //  use warp_affine grid
   WarpF_ftf = 1;                                                                //  1st warp flag

   zdialog_run(zd,WarpF_dialog_event,"save");                                    //  run dialog, parallel

   takeMouse(WarpF_mousefunc,dragcursor);                                        //  connect mouse function
   return;
}


//  dialog event and completion callback function

int WarpF_dialog_event(zdialog *zd, cchar *event)
{
   float       scale;
   int         ww, hh;

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()

   if (strmatch(event,"grid")) m_gridlines(0,"grid 5");                          //  grid settings dialog

   if (! zd->zstat) return 1;                                                    //  wait for completion

   currgrid = 0;                                                                 //  restore normal grid settings

   if (zd->zstat != 1 || ! CEF->Fmods) {
      edit_cancel(0); 
      return 1;
   }

   ww = E3pxm->ww;                                                               //  preview image dimensions
   hh = E3pxm->hh;

   edit_fullsize();                                                              //  get full-size images

   scale = 1.0 * (E3pxm->ww + E3pxm->hh) / (ww + hh);                            //  preview to full-size scale factor

   WarpF_old[0][0] = WarpF_old[0][0] * scale;                                    //  re-scale new and old points
   WarpF_old[0][1] = WarpF_old[0][1] * scale;
   WarpF_old[1][0] = WarpF_old[1][0] * scale;
   WarpF_old[1][1] = WarpF_old[1][1] * scale;
   WarpF_old[2][0] = WarpF_old[2][0] * scale;
   WarpF_old[2][1] = WarpF_old[2][1] * scale;

   WarpF_new[0][0] = WarpF_new[0][0] * scale;
   WarpF_new[0][1] = WarpF_new[0][1] * scale;
   WarpF_new[1][0] = WarpF_new[1][0] * scale;
   WarpF_new[1][1] = WarpF_new[1][1] * scale;
   WarpF_new[2][0] = WarpF_new[2][0] * scale;
   WarpF_new[2][1] = WarpF_new[2][1] * scale;

   WarpF_warpfunc();                                                             //  warp full-size image

   edit_done(0);
   return 1;
}


//  WarpF mouse function

void  WarpF_mousefunc()
{
   int      mdx1, mdy1, mdx2, mdy2;
   float    x1o, y1o, x2o, y2o, x3o, y3o;
   float    x1n, y1n, x2n, y2n, x3n, y3n;
   float    a, b, c, d, e, f;

   if (! Mxdrag && ! Mydrag) return;

   mdx1 = Mxdown;                                                                //  mouse drag origin
   mdy1 = Mydown;
   mdx2 = Mxdrag;                                                                //  mouse drag position
   mdy2 = Mydrag;

   Mxdown = Mxdrag;                                                              //  reset origin for next time
   Mydown = Mydrag;

   x1n = mdx1;                                                                   //  point 1 = drag origin
   y1n = mdy1;
   x2n = E3pxm->ww - x1n;                                                        //  point 2 = mirror of point1
   y2n = E3pxm->hh - y1n;
   x3n = E3pxm->ww * (y2n / E3pxm->hh);
   y3n = E3pxm->hh * (1.0 - (x2n / E3pxm->ww));

   if (WarpF_ftf)                                                                //  first warp
   {
      WarpF_ftf = 0;
      x1o = x1n;                                                                 //  old = current positions
      y1o = y1n;
      x2o = x2n;
      y2o = y2n;
      x3o = x3n;
      y3o = y3n;
   }
   else
   {
      WarpF_invert(WarpF_coeff,WarpF_Icoeff);                                    //  get inverse coefficients
      a = WarpF_Icoeff[0];
      b = WarpF_Icoeff[1];
      c = WarpF_Icoeff[2];
      d = WarpF_Icoeff[3];
      e = WarpF_Icoeff[4];
      f = WarpF_Icoeff[5];

      x1o = a * x1n + b * y1n + c;                                               //  compute old from current positions
      y1o = d * x1n + e * y1n + f;
      x2o = a * x2n + b * y2n + c;
      y2o = d * x2n + e * y2n + f;
      x3o = a * x3n + b * y3n + c;
      y3o = d * x3n + e * y3n + f;
   }

   WarpF_old[0][0] = x1o;                                                        //  set up 3 old points and corresponding
   WarpF_old[0][1] = y1o;                                                        //    new points for affine translation
   WarpF_old[1][0] = x2o;
   WarpF_old[1][1] = y2o;
   WarpF_old[2][0] = x3o;
   WarpF_old[2][1] = y3o;

   x1n = mdx2;                                                                   //  point 1 new position = drag position
   y1n = mdy2;
   x2n = E3pxm->ww - x1n;                                                        //  point 2 new = mirror of point1 new
   y2n = E3pxm->hh - y1n;

   WarpF_new[0][0] = x1n;                                                        //  3 new points
   WarpF_new[0][1] = y1n;
   WarpF_new[1][0] = x2n;
   WarpF_new[1][1] = y2n;
   WarpF_new[2][0] = x3n;
   WarpF_new[2][1] = y3n;

   WarpF_warpfunc();                                                             //  do the warp

   Mxdrag = Mydrag = 0;
   return;
}


//  warp image and accumulate warp memory

void  WarpF_warpfunc()
{
   void * WarpF_wthread(void *);

   WarpF_affine(WarpF_old, WarpF_new, WarpF_coeff);                              //  get coefficients for forward transform
   WarpF_invert(WarpF_coeff, WarpF_Icoeff);                                      //  get coefficients for reverse transform

   do_wthreads(WarpF_wthread,NWT);                                               //  worker threads

   CEF->Fmods++;
   CEF->Fsaved = 0;
   Fpaint2();                                                                    //  update window
   return;
}


//  working thread to process the pixels

void * WarpF_wthread(void *arg)
{
   int      index = *((int *) arg);
   float    a, b, c, d, e, f;
   int      px3, py3, vstat;
   float    px1, py1;
   float    vpix1[4], *pix3;
   int      nc = E1pxm->nc, pcc = nc * sizeof(float);

   a = WarpF_Icoeff[0];                                                          //  coefficients to map output pixels
   b = WarpF_Icoeff[1];                                                          //    to corresponding input pixels
   c = WarpF_Icoeff[2];
   d = WarpF_Icoeff[3];
   e = WarpF_Icoeff[4];
   f = WarpF_Icoeff[5];

   for (py3 = index; py3 < E3pxm->hh; py3 += NWT)                                //  process all pixels
   for (px3 = 0; px3 < E3pxm->ww; px3++)
   {
      px1 = a * px3 + b * py3 + c;                                               //  corresponding input pixel
      py1 = d * px3 + e * py3 + f;

      vstat = vpixel(E1pxm,px1,py1,vpix1);                                       //  input virtual pixel
      pix3 = PXMpix(E3pxm,px3,py3);                                              //  output pixel
      if (vstat) memcpy(pix3,vpix1,pcc);
      else memset(pix3,0,pcc);                                                   //  voided pixel
   }

   pthread_exit(0);                                                              //  exit thread
}


/********************************************************************************

   Compute affine transformation of an image (warp image).

   Given 3 new (warped) positions for 3 image points, derive the
   coefficients of the translation function to warp the entire image.

   Inputs:
      pold[3][2]  (x,y) coordinates for 3 points in original image
      pnew[3][2]  (x,y) coordinates for same points in warped image

   Output:
      coeff[6]  coefficients of translation function which can be used
                to convert all image points to their warped positions

   If coeff[6] = (a, b, c, d, e, f) then the following formula
   can be used to convert an image point to its warped position:

      Xnew = a * Xold + b * Yold + c
      Ynew = d * Xold + e * Yold + f

*********************************************************************************/

void WarpF_affine(float pold[3][2], float pnew[3][2], float coeff[6])
{
   float    x11, y11, x12, y12, x13, y13;                                        //  original points
   float    x21, y21, x22, y22, x23, y23;                                        //  moved points
   float    a, b, c, d, e, f;                                                    //  coefficients
   float    A1, A2, B1, B2, C1, C2;

   x11 = pold[0][0];
   y11 = pold[0][1];
   x12 = pold[1][0];
   y12 = pold[1][1];
   x13 = pold[2][0];
   y13 = pold[2][1];

   x21 = pnew[0][0];
   y21 = pnew[0][1];
   x22 = pnew[1][0];
   y22 = pnew[1][1];
   x23 = pnew[2][0];
   y23 = pnew[2][1];

   A1 = x11 - x12;
   A2 = x12 - x13;
   B1 = y11 - y12;
   B2 = y12 - y13;
   C1 = x21 - x22;
   C2 = x22 - x23;

   a = (B1 * C2 - B2 * C1) / (A2 * B1 - A1 * B2);
   b = (A1 * C2 - A2 * C1) / (A1 * B2 - A2 * B1);
   c = x23 - a * x13 - b * y13;

   C1 = y21 - y22;
   C2 = y22 - y23;

   d = (B1 * C2 - B2 * C1) / (A2 * B1 - A1 * B2);
   e = (A1 * C2 - A2 * C1) / (A1 * B2 - A2 * B1);
   f = y23 - d * x13 - e * y13;

   coeff[0] = a;
   coeff[1] = b;
   coeff[2] = c;
   coeff[3] = d;
   coeff[4] = e;
   coeff[5] = f;

   return;
}


/********************************************************************************

   Invert affine transform

   Input:
      coeff[6]  coefficients of translation function to convert
                image points to their warped positions
   Output:
      Icoeff[6]  coefficients of translation function to convert
                 warped image points to their original positions

   If Icoeff[6] = (a, b, c, d, e, f) then the following formula can be
      used to translate a warped image point to its original position:

      Xold = a * Xnew + b * Ynew + c
      Yold = d * Xnew + e * Ynew + f

*********************************************************************************/

void WarpF_invert(float coeff[6], float Icoeff[6])
{
   float    a, b, c, d, e, f, Z;

   a = coeff[0];
   b = coeff[1];
   c = coeff[2];
   d = coeff[3];
   e = coeff[4];
   f = coeff[5];

   Z = 1.0 / (a * e - b * d);

   Icoeff[0] = e * Z;
   Icoeff[1] = - b * Z;
   Icoeff[2] = Z * (b * f - c * e);
   Icoeff[3] = - d * Z;
   Icoeff[4] = a * Z;
   Icoeff[5] = Z * (c * d - a * f);

   return;
}


/********************************************************************************/

//  Unwarp closeup face photo - shrink magnified areas closest to camera

int         unwarpCU_started;
int         unwarpCU_areanumber;
float       unwarpCU_warpval;
int         unwarpCU_cx, unwarpCU_cy;
editfunc    EFunwarpCU;

int   unwarpCU_dialog_event(zdialog *zd, cchar *event);
void  unwarpCU_warpfunc();
void  unwarpCU_mousefunc();

//  menu function

void m_unwarp_closeup(GtkWidget *, cchar *)
{
   cchar  *unwarpCU_message = E2X(
             " Use Select Area to select a face. \n"
             " Click on the center of distortion. \n"
             " Move the slider. \n");

   F1_help_topic = "unwarp closeup";

   EFunwarpCU.menufunc = m_unwarp_closeup;
   EFunwarpCU.funcname = "unwarp_closeup";
   EFunwarpCU.Farea = 2;                                                         //  select area usable
   EFunwarpCU.mousefunc = unwarpCU_mousefunc;                                    //  mouse function
   if (! edit_setup(EFunwarpCU)) return;                                         //  setup edit

/***
       ______________________________________
      |           Unwarp Closeup             |
      |                                      |
      |  Use Select Area to select a face.   |
      |  Click on the center of distortion.  |
      |  Move the slider.                    |
      |                                      |
      |  [===============[]================] |
      |                                      |
      |                      [done] [cancel] |
      |______________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Unwarp Closeup"),Mwin,Bdone,Bcancel,null);
   EFunwarpCU.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",unwarpCU_message,"space=3");
   zdialog_add_widget(zd,"hbox","hbw","dialog",0,"space=5");
   zdialog_add_widget(zd,"hscale","warpval","hbw","0.0|1.0|0.01|0.0","space=5|expand");

   takeMouse(unwarpCU_mousefunc,dragcursor);                                     //  connect mouse function

   unwarpCU_started = 0;
   unwarpCU_areanumber = 0;

   zdialog_run(zd,unwarpCU_dialog_event,"save");                                 //  run dialog, parallel
   return;
}


//  dialog event and completion callback function

int unwarpCU_dialog_event(zdialog * zd, cchar *event)
{
   int      init = 0;

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()

   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   if (! sa_validate()) init = 1;                                                //  area invalid for curr. image file

   if (sa_stat != 3 || sa_mode == mode_image)                                    //  no select area active
      init = 1;

   if (unwarpCU_started && unwarpCU_areanumber != areanumber)                    //  select area changed
      init = 1;

   if (init) {
      unwarpCU_started = 0;
      unwarpCU_areanumber = 0;
      return 1;
   }
   
   if (strmatch(event,"focus"))                                                  //  reconnect mouse
      takeMouse(unwarpCU_mousefunc,dragcursor);

   if (strmatch(event,"warpval")) {                                              //  slider movement
      zdialog_fetch(zd,"warpval",unwarpCU_warpval);
      unwarpCU_warpfunc();
   }

   return 1;
}


//  mouse function

void unwarpCU_mousefunc()
{
   if (LMclick) {
      unwarpCU_cx = Mxclick;                                                     //  capture central point
      unwarpCU_cy = Myclick;
   }
   else if (Mxdrag || Mydrag) {
      unwarpCU_cx = Mxdrag;
      unwarpCU_cy = Mydrag;
   }
   else return;
   
   LMclick = Mxdrag = Mydrag = 0;

   if (sa_stat != 3 || sa_mode == mode_image) {                                  //  no select area active
      zmessageACK(Mwin,E2X("no active Select Area"));
      unwarpCU_started = 0;
      Mdrag = 0;
      return;
   }
   
   unwarpCU_started = 1;                                                         //  unwarp can proceed
   unwarpCU_areanumber = areanumber;
   sa_edgecalc();                                                                //  calculate area edge distances
   
   unwarpCU_warpfunc();
   return;
}


//  warp image according to slider position

void unwarpCU_warpfunc()
{
   int         ii, px, py, ww, vstat;
   float       hsize, vsize, hpos, vpos;
   float       warpval = unwarpCU_warpval;
   float       ed, cx, cy, dx, dy;
   float       vpix[4], *pix3;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   if (sa_stat != 3) return;                                                     //  area erased
   
   hsize = sa_maxx - sa_minx;                                                    //  area size
   vsize = sa_maxy - sa_miny;
   
   cx = unwarpCU_cx;                                                             //  unwarp center (nose tip)
   cy = unwarpCU_cy;

   ww = E1pxm->ww;

   for (py = sa_miny; py < sa_maxy; py++)                                        //  loop all pixels in area
   for (px = sa_minx; px < sa_maxx; px++)
   {
      ii = py * ww + px;
      if (sa_pixmap[ii] < 1) continue;                                           //  pixel outside area
      
      hpos = 2.0 * (px - cx) / hsize;                                            //  horizontal pixel position, -1 ... +1
      vpos = 2.0 * (py - cy) / vsize;                                            //  vertical pixel position, -1 ... +1

      ed = sa_pixmap[ii];                                                        //  pixel edge distance

      hpos = hpos * ed / hsize;                                                  //  scale pixel position 
      vpos = vpos * ed / vsize;
      
      dx = sinf(PI * hpos);                                                      //  pixel displacement, -1 ... +1
      dy = sinf(PI * vpos);
      
      dx = dx * 0.1 * warpval * hsize;                                           //  pixel displacement, -10% ... +10%
      dy = dy * 0.1 * warpval * vsize;
      
      vstat = vpixel(E1pxm,px+dx,py+dy,vpix);                                    //  input virtual pixel
      if (vstat) {
         pix3 = PXMpix(E3pxm,px,py);                                             //  output pixel
         memcpy(pix3,vpix,pcc);
      }
   }

   Fpaint3(sa_minx,sa_miny,hsize,vsize,0);

   CEF->Fmods++;
   CEF->Fsaved = 0;

   return;
}


/********************************************************************************/

//  Flatten a photographed book page.
//  Compensate for page curvature at the center binding.

editfunc    EFflatbook;                                                          //  edit function data

namespace flatbook {
   int      Tmx[20], Tmy[20], Bmx[20], Bmy[20];                                  //  top/bottom mouse click points
   int      Tnm = 0, Bnm = 0;                                                    //  top/bottom click points counts
   int      Tbase, Bbase;                                                        //  top/bottom points, low values
   int      ww, hh;                                                              //  image dimensions
   int      E3warped;                                                            //  flag, E3 image is warped
   double   *Tfy, *Bfy;                                                          //  derived top/bottom y-shifts [ww]
   double   *Tfx, *Bfx;                                                          //  derived top/bottom x-shifts [ww]
   double   Tstretch, Bstretch;                                                  //  top/bottom x-shift stretch factors
}


//  menu function

void m_flatbook(GtkWidget *, const char *)
{
   using namespace flatbook;

   int    flatbook_dialog_event(zdialog* zd, const char *event);
   void * flatbook_thread(void *);
   void   flatbook_mousefunc();
   void   flatbook_draw();

   cchar    *title = E2X("Flatten Book Page");
   cchar    *guide = E2X("Trim image to isolate one page. \n"
                         "Map top and bottom edges with \n"
                         "4+ mouse clicks, then flatten: ");
   cchar    *stretch = E2X("Stretch curved-down surfaces:");
   zdialog  *zd;

   F1_help_topic = "flatten book";

   EFflatbook.menufunc = m_flatbook;
   EFflatbook.funcname = "flatbook";                                             //  func name, no preview, no area
   EFflatbook.threadfunc = flatbook_thread;
   EFflatbook.mousefunc = flatbook_mousefunc;
   if (! edit_setup(EFflatbook)) return;                                         //  setup edit

   int cc = E1pxm->ww * sizeof(double);                                          //  allocate memory
   Tfy = (double *) zmalloc(cc);
   Bfy = (double *) zmalloc(cc);
   Tfx = (double *) zmalloc(cc);
   Bfx = (double *) zmalloc(cc);

   if (ww != E1pxm->ww) Tnm = Bnm = 0;                                           //  clear prior points if image
   if (hh != E1pxm->hh) Tnm = Bnm = 0;                                           //    size is different

   ww = E1pxm->ww;                                                               //  set image size
   hh = E1pxm->hh;
   E3warped = 0;                                                                 //  no E3 warp yet

/***
           _________________________________
          |       Flatten Book Page         |
          |                                 |
          | Trim image to isolate one page. |
          | Map top and bottom edges with   |
          | 4+ mouse clicks, then flatten:  |
          |   [clear]  [flatten]  [undo]    |
          |                                 |
          | Stretch curved-down surfaces:   |
          |  Top: ========[]==============  |
          |  Bottom: =========[]==========  |
          |                                 |
          |              [done] [cancel]    |
          |_________________________________|

***/

   zd = zdialog_new(title,Mwin,Bdone,Bcancel,null);                              //  flatbook dialog
   EFflatbook.zd = zd;

   zdialog_add_widget(zd,"hbox","hbg","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labg","hbg",guide,"space=8");
   zdialog_add_widget(zd,"hbox","hbf","dialog");
   zdialog_add_widget(zd,"button","clear","hbf",Bclear,"space=10");
   zdialog_add_widget(zd,"button","flatten","hbf",Bflatten,"space=10");
   zdialog_add_widget(zd,"button","undo","hbf",Bundo,"space=10");
   zdialog_add_widget(zd,"hbox","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbs1","dialog");
   zdialog_add_widget(zd,"label","labs1","hbs1",stretch,"space=8");
   zdialog_add_widget(zd,"hbox","hbs2","dialog");
   zdialog_add_widget(zd,"label","labs2","hbs2",E2X("Top:"),"space=8");
   zdialog_add_widget(zd,"hscale","top","hbs2","1|30|0.01|1","expand|space=5");
   zdialog_add_widget(zd,"hbox","hbs3","dialog");
   zdialog_add_widget(zd,"label","labs3","hbs3",E2X("Bottom:"),"space=8");
   zdialog_add_widget(zd,"hscale","bottom","hbs3","1|30|0.01|1","expand|space=5");

   zdialog_restore_inputs(zd);                                                   //  restore prior inputs

   zdialog_resize(zd,300,0);
   zdialog_run(zd,flatbook_dialog_event,"save");                                 //  run dialog - parallel

   takeMouse(flatbook_mousefunc,dragcursor);                                     //  connect mouse function

   if (Tnm + Bnm > 0) flatbook_draw();
   return;
}


//  flatbook dialog event and completion function

int flatbook_dialog_event(zdialog *zd, const char *event)                        //  flatbook dialog event function
{
   using namespace flatbook;

   void flatbook_draw();

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()

   if (zd->zstat) {
      zfree(Tfy);                                                                //  free memory
      zfree(Bfy);
      zfree(Tfx);
      zfree(Bfx);
      if (zd->zstat == 1) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  cancel or destroy
      return 1;
   }

   if (strmatch(event,"clear")) {                                                //  clear all mouse points
      Tnm = Bnm = 0;
      edit_undo();
      flatbook_draw();
   }

   if (zstrstr("flatten top bottom",event)) {
      if (Tnm < 4 || Bnm < 4) return 1;
      zdialog_fetch(zd,"top",Tstretch);
      zdialog_fetch(zd,"bottom",Bstretch);
      signal_thread();                                                           //  trigger update thread
   }

   if (strmatch(event,"undo")) {
      edit_undo();
      E3warped = 0;
      flatbook_draw();
   }

   if (strmatch(event,"line_color")) flatbook_draw();                            //  refresh lines after window redraw

   return 1;
}


//  flatbook mouse function

void flatbook_mousefunc()
{
   using namespace flatbook;

   void flatbook_draw();

   int      ii, jj, mx, my, dist;
   int      Fadd = 0, close;

   if (! (LMclick || RMclick || Mxdrag || Mydrag)) return;                       //  ignore mouse movement

   if (E3warped) return;                                                         //  E3 image warped, ignore mouse

   if (LMclick || RMclick) {                                                     //  left or right mouse click
      mx = Mxclick;
      my = Myclick;
      if (LMclick) Fadd = 1;
      LMclick = RMclick = 0;
   }
   else {                                                                        //  mouse drag
      mx = Mxdrag;
      my = Mydrag;
      Mxdrag = Mydrag = 0;
      Fadd = 1;
   }

   close = 0.01 * ww;                                                            //  1% of image size

   if (my < hh/2) {
      for (ii = 0; ii < Tnm; ii++) {                                             //  compare mouse position with
         dist = abs(mx-Tmx[ii]);                                                 //    existing top points
         if (dist < close) {
            Tnm--;                                                               //  delete any too close
            for (jj = ii--; jj < Tnm; jj++) {
               Tmx[jj] = Tmx[jj+1];
               Tmy[jj] = Tmy[jj+1];
            }
         }
      }
   }
   else {
      for (ii = 0; ii < Bnm; ii++) {                                             //  same for bottom points
         dist = abs(mx-Bmx[ii]);
         if (dist < close) {
            Bnm--;
            for (jj = ii--; jj < Bnm; jj++) {
               Bmx[jj] = Bmx[jj+1];
               Bmy[jj] = Bmy[jj+1];
            }
         }
      }
   }

   if (Fadd)                                                                     //  add new mouse position
   {
      if (my < hh/2) {                                                           //  add to top points
         if (Tnm == 20) return;
         for (ii = 0; ii < Tnm; ii++)
            if (mx < Tmx[ii]) break;
         for (jj = Tnm; jj > ii; jj--) {
            Tmx[jj] = Tmx[jj-1];
            Tmy[jj] = Tmy[jj-1];
         }
         Tmx[ii] = mx;
         Tmy[ii] = my;
         Tnm++;
      }
      else {                                                                     //  add to bottom points
         if (Bnm == 20) return;
         for (ii = 0; ii < Bnm; ii++)
            if (mx < Bmx[ii]) break;
         for (jj = Bnm; jj > ii; jj--) {
            Bmx[jj] = Bmx[jj-1];
            Bmy[jj] = Bmy[jj-1];
         }
         Bmx[ii] = mx;
         Bmy[ii] = my;
         Bnm++;
      }
   }

   edit_undo();                                                                  //  erase and redraw
   flatbook_draw();

   return;
}


//  generate spline curves from mouse points and draw on the image

void flatbook_draw()
{
   using namespace flatbook;

   float    Tnx[20], Tny[20], Bnx[20], Bny[20];                                  //  top/bottom spline nodes
   float    *ppix3;
   int      topmax, topmin, bottmax, bottmin;                                    //  limits of image update areas
   int      ii, px, py, qx, qy, npq;

   topmax = topmin = bottmax = bottmin = 0;
   npq = 3;                                                                      //  pixel block for drawing nodes

   if (Tnm > 0) topmax = topmin = Tmy[0];

   for (ii = 0; ii < Tnm; ii++)                                                  //  draw top mouse points
   {
      px = Tmx[ii];
      py = Tmy[ii];

      for (qy = py-npq; qy <= py+npq; qy++)
      for (qx = px-npq; qx <= px+npq; qx++) {
         if (qx < 0 || qx > ww-1) continue;
         if (qy < 0 || qy > hh-1) continue;
         ppix3 = PXMpix(E3pxm,qx,qy);
         ppix3[0] = LINE_COLOR[0];
         ppix3[1] = LINE_COLOR[1];
         ppix3[2] = LINE_COLOR[2];
         if (qy > topmax) topmax = qy;
         if (qy < topmin) topmin = qy;
      }
   }

   if (Bnm > 0) bottmax = bottmin = Bmy[0];

   for (ii = 0; ii < Bnm; ii++)                                                  //  draw bottom mouse points
   {
      px = Bmx[ii];
      py = Bmy[ii];

      for (qy = py-npq; qy <= py+npq; qy++)
      for (qx = px-npq; qx <= px+npq; qx++) {
         if (qx < 0 || qx > ww-1) continue;
         if (qy < 0 || qy > hh-1) continue;
         ppix3 = PXMpix(E3pxm,qx,qy);
         ppix3[0] = LINE_COLOR[0];
         ppix3[1] = LINE_COLOR[1];
         ppix3[2] = LINE_COLOR[2];
         if (qy > bottmax) bottmax = qy;
         if (qy < bottmin) bottmin = qy;
      }
   }

   if (Tnm > 3)                                                                  //  top mouse points
   {
      Tbase = Tmy[0];                                                            //  find lowest top point
      for (ii = 0; ii < Tnm; ii++)
         if (Tmy[ii] < Tbase) Tbase = Tmy[ii];

      for (ii = 0; ii < Tnm; ii++) {                                             //  copy top points to spline nodes
         Tnx[ii] = Tmx[ii];                                                      //    and convert to 0 base
         Tny[ii] = Tmy[ii] - Tbase;
      }

      spline1(Tnm,Tnx,Tny);                                                      //  generate spline curve

      for (px = 0; px < ww; px++)                                                //  generate top curve y shifts
         Tfy[px] = spline2(px);

      for (px = 0; px < ww; px++) {                                              //  draw top curve over image
         py = Tfy[px] + Tbase;
         if (py < 0 || py > hh-1) continue;
         ppix3 = PXMpix(E3pxm,px,py);
         ppix3[0] = LINE_COLOR[0];
         ppix3[1] = LINE_COLOR[1];
         ppix3[2] = LINE_COLOR[2];
         if (py < topmin) topmin = py;
         if (py > topmax) topmax = py;
      }
   }

   if (Bnm > 3)                                                                  //  bottom mouse points
   {
      Bbase = Bmy[0];                                                            //  find lowest bottom point
      for (ii = 0; ii < Bnm; ii++)
         if (Bmy[ii] < Bbase) Bbase = Bmy[ii];

      for (ii = 0; ii < Bnm; ii++) {                                             //  copy bottom points to spline nodes
         Bnx[ii] = Bmx[ii];                                                      //    and convert to 0 base
         Bny[ii] = Bmy[ii] - Bbase;
      }

      spline1(Bnm,Bnx,Bny);                                                      //  generate spline curve

      for (px = 0; px < ww; px++)                                                //  generate bottom curve y shifts
         Bfy[px] = spline2(px);

      for (px = 0; px < ww; px++) {                                              //  draw bottom curve over image
         py = Bfy[px] + Bbase;
         if (py < 0 || py > hh-1) continue;
         ppix3 = PXMpix(E3pxm,px,py);
         ppix3[0] = LINE_COLOR[0];
         ppix3[1] = LINE_COLOR[1];
         ppix3[2] = LINE_COLOR[2];
         if (py < bottmin) bottmin = py;
         if (py > bottmax) bottmax = py;
      }
   }

   if (Tnm) Fpaint3(0,topmin,ww,topmax-topmin,0);                                //  paint modified areas
   if (Bnm) Fpaint3(0,bottmin,ww,bottmax-bottmin,0);

   if (Tnm + Bnm) CEF->Fmods++;                                                  //  necessary
   CEF->Fsaved = 0;

   return;
}


//  flatbook thread function - warp image based on input parameters

void * flatbook_thread(void *)
{
   using namespace flatbook;

   void  * flatbook_wthread(void *arg);                                          //  worker thread

   int      px;
   double   Tslope, Bslope, slope, Rt, Rb;

   while (true)
   {
      thread_idle_loop();

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      E3warped = 1;                                                              //  flag, E3 image is warped

      Tfx[0] = Bfx[0] = 0;

      for (px = 1; px < ww; px++)                                                //  loop page width
      {
         Tslope = fabs(Tfy[px] - Tfy[px-1]);                                     //  slope of top function
         Bslope = fabs(Bfy[px] - Bfy[px-1]);                                     //  slope of bottom function

         slope = Tslope * Tstretch;                                              //  mean slope, stretched
         slope = cos(atan(slope));
         Tfx[px] = Tfx[px-1] + slope;                                            //  resulting pixel x-shift

         slope = Bslope * Bstretch;
         slope = cos(atan(slope));
         Bfx[px] = Bfx[px-1] + slope;
      }

      Rt = (ww-1) / Tfx[ww-1];                                                   //  make overall width the same
      Rb = (ww-1) / Bfx[ww-1];
      for (px = 0; px < ww; px++) {
         Tfx[px] = Rt * Tfx[px];                                                 //  (using separate loops causes
         Bfx[px] = Rb * Bfx[px];                                                 //      GCC optimization bug)
      }

      do_wthreads(flatbook_wthread,NWT);                                         //  worker threads

      CEF->Fmods++;                                                              //  image is modified
      CEF->Fsaved = 0;

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

   return 0;
}


void * flatbook_wthread(void *arg)
{
   using namespace flatbook;

   int      index = *((int *) (arg));
   int      px, py, vstat;
   double   Rt, Rb;
   float    *pix3, vpix[4], vpx, vpy;
   int      nc = E1pxm->nc, pcc = nc * sizeof(float);

   for (py = index; py < hh; py += NWT)                                          //  loop from top to bottom
   {
      Rt = 1.0 - 1.0 * py / hh;                                                  //  Rt from 1 to 0  - top weight
      Rb = 1.0 - Rt;                                                             //  Rb from 0 to 1  - bottom weight

      for (px = 0; px < ww; px++)                                                //  loop page width
      {
         pix3 = PXMpix(E3pxm,px,py);                                             //  output pixel

         vpx = Rt * Tfx[px] + Rb * Bfx[px];                                      //  source pixel for (px,py)
         vpy = py + Rt * Tfy[int(vpx)] + Rb * Bfy[int(vpx)];

         vstat = vpixel(E1pxm,vpx,vpy,vpix);                                     //  get source pixel
         if (vstat) memcpy(pix3,vpix,pcc);
         else memset(pix3,0,pcc);                                                //  off page, void pixel
      }
   }

   pthread_exit(0);
}


/********************************************************************************/

//  Rescale an image while leaving selected areas unchanged.

namespace area_rescale_names
{
   editfunc    EFarea_rescale;

   int      Fsetups = 0;
   int      dragx, dragy;
   int      E3ww, E3hh;
   char     *sqrow, *sqcol;
   int      Nsqrow, Nsqcol;
   int      *npx, *npy;

   int    dialog_event(zdialog *zd, cchar *event);
   void   setups();
   void   cleanups();
   void   mousefunc();
   void   warpfunc();
   void   *warpthread(void *);
}


//  menu function

void m_area_rescale(GtkWidget *, cchar *)
{
   using namespace area_rescale_names;

   cchar  *message = E2X(" Select areas to remain unchanged. \n"
                         " Pull image from upper left corner. \n"
                         " When finished, press [done].");

   F1_help_topic = "area rescale";

   EFarea_rescale.menufunc = m_area_rescale;
   EFarea_rescale.funcname = "area_rescale";
   EFarea_rescale.Farea = 2;                                                     //  select area usable
   EFarea_rescale.mousefunc = mousefunc;                                         //  mouse function
   if (! edit_setup(EFarea_rescale)) return;                                     //  setup edit

   PXM_addalpha(E0pxm);
   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

   zdialog *zd = zdialog_new(E2X("Area Rescale"),Mwin,Bproceed,Bdone,Bcancel,null);
   EFarea_rescale.zd = zd;
   zdialog_add_widget(zd,"label","lab1","dialog",message,"space=3");

   zdialog_run(zd,dialog_event,"save");                                          //  run dialog, parallel
   return;
}


//  dialog event and completion callback function

int area_rescale_names::dialog_event(zdialog * zd, cchar *event)
{
   using namespace area_rescale_names;

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   
   if (! zd->zstat) return 1;                                                    //  wait for completion
   
   if (zd->zstat == 1) {                                                         //  [proceed]
      zd->zstat = 0;                                                             //  keep dialog active
      if (sa_stat != 3)
         zmessageACK(Mwin,E2X("select areas first"));
      else setups();                                                             //  start drag and warp
      return 1;
   }
   
   if (zd->zstat != 2 || dragx + dragy == 0) {                                   //  [cancel] or no change
      edit_cancel(0);
      cleanups();
      return 1;
   }

   edit_done(0);                                                                 //  [done]
   cleanups();
   m_trim_rotate(0,"auto");                                                      //  set up automatic trim
   return 1;
}


//  do setups based on select area data

void area_rescale_names::setups()
{
   int      ii, spx, spy, sum;

   cleanups();                                                                   //  free prior if any

   dragx = dragy = 0;                                                            //  no drag data

   E3ww = E3pxm->ww;                                                             //  image dimensions
   E3hh = E3pxm->hh;

   sqrow = (char *) zmalloc(E3hh);                                               //  maps squishable rows/cols
   sqcol = (char *) zmalloc(E3ww);
   memset(sqrow,1,E3hh);                                                         //  mark all rows/cols squishable
   memset(sqcol,1,E3ww);
   
   for (spy = 0; spy < E3hh; spy++)                                              //  loop all source pixels
   for (spx = 0; spx < E3ww; spx++)
   {
      ii = spy * E3ww + spx;                                                     //  pixel within area?
      if (sa_pixmap[ii]) sqrow[spy] = sqcol[spx] = 0;                            //  mark row/col non-squishable
   }
   
   Nsqrow = Nsqcol = 0;
   for (spy = 0; spy < E3hh; spy++)                                              //  count total squishable rows/cols
      Nsqrow += sqrow[spy];
   for (spx = 0; spx < E3ww; spx++)
      Nsqcol += sqcol[spx];

   npx = (int *) zmalloc(E3ww * sizeof(int));                                    //  count of squishable rows/cols
   npy = (int *) zmalloc(E3hh * sizeof(int));                                    //    predeeding a given row/col
   
   for (sum = spx = 0; spx < E3ww; spx++)
   {
      if (sqcol[spx]) sum++;
      npx[spx] = sum;
   }

   for (sum = spy = 0; spy < E3hh; spy++)
   {
      if (sqrow[spy]) sum++;
      npy[spy] = sum;                                                            //  squishable rows < spy
   }

   Fsetups = 1;
   sa_clear();                                                                   //  clear area
   takeMouse(mousefunc,dragcursor);                                              //  connect mouse function
   return;
}


//  free allocated memory

void area_rescale_names::cleanups()
{
   if (! Fsetups) return;
   Fsetups = 0;
   zfree(sqrow);
   zfree(sqcol);   
   zfree(npx);
   zfree(npy);
   return;
}


//  mouse function

void area_rescale_names::mousefunc()
{
   using namespace area_rescale_names;
   
   float    R;
   
   if (Mxdrag || Mydrag)                                                         //  mouse drag underway
   {
      R = 1.0 * Mxdown / E3ww;                                                   //  ignore drag not from NW corner
      if (R > 0.2) return;
      R = 1.0 * Mydown / E3hh;
      if (R > 0.2) return;
      dragx = Mxdrag - Mxdown;                                                   //  drag amount
      dragy = Mydrag - Mydown;
      warpfunc();                                                                //  drag image
      Mxdrag = Mydrag = 0;
   }

   return;
}


//  warp function

void area_rescale_names::warpfunc()
{
   using namespace area_rescale_names;

   do_wthreads(warpthread,NWT);                                                  //  worker threads

   CEF->Fmods++;
   CEF->Fsaved = 0;

   Fpaint2();                                                                    //  update window
   return;
}


//  warp thread

void * area_rescale_names::warpthread(void *arg)
{
   using namespace area_rescale_names;

   int      index = *((int *) (arg));
   int      spx, spy, dpx, dpy;
   float    Rx, Ry;
   float    *spix, *dpix;
   int      nc = E1pxm->nc, pcc = nc * sizeof(float);
   
   for (spy = index; spy < E3hh; spy += NWT)                                     //  loop all source pixels
   for (spx = 0; spx < E3ww; spx++)
   {
      if (spx < dragx || spy < dragy) {                                          //  pixels < dragx/dragy:
         spix = PXMpix(E3pxm,spx,spy);                                           //    black, transparent
         memset(spix,0,pcc);
      }

      Rx = 1.0 * npx[spx] / Nsqcol;                                              //  squishable pixel ratios, 0 - 1.0
      Ry = 1.0 * npy[spy] / Nsqrow;
      
      dpx = spx + dragx * (1.0 - Rx);                                            //  destination pixel
      dpy = spy + dragy * (1.0 - Ry);

      if (dpx < 0 || dpx > E3ww-1) continue;                                     //  necessary, why? 
      if (dpy < 0 || dpy > E3hh-1) continue;
      
      dpix = PXMpix(E3pxm,dpx,dpy);                                              //  source pixel >> destination pixel
      spix = PXMpix(E1pxm,spx,spy);
      memcpy(dpix,spix,pcc);
   }

   pthread_exit(0);
}


/********************************************************************************/

//  make waves menu function
//  distort the image with a wave pattern
//  independent horizontal and vertical wavelengths
//  variance option: make waves more or less irregular
//  perspective option: wavelengths lengthen going down

namespace waves_names
{
   editfunc EFwaves;
   int      ww, hh;                          //  image dimensions
   int      WLV, WLH;                        //  vertical and horizontal wavelengths
   int      AMPV, AMPH;                      //  vertical and horizontal amplitudes
   int      VARV, VARH;                      //  vertical and horizontal variance
   int      PERSP;                           //  perspective 0-100
}


//  menu function

void m_waves(GtkWidget *, const char *)
{
   using namespace waves_names;

   int    waves_dialog_event(zdialog* zd, const char *event);
   void * waves_thread(void *);

   F1_help_topic = "make waves";

   EFwaves.menufunc = m_waves;
   EFwaves.funcname = "make_waves";
   EFwaves.Farea = 2;                                                            //  select area usable
   EFwaves.threadfunc = waves_thread;

   if (! edit_setup(EFwaves)) return;

/***
          ___________________________________
         |           Make waves              |
         |                                   |
         |             Horizontal  Vertical  |
         |  wavelength [___]        [___]    |
         |  amplitude  [___]        [___]    |
         |  variance   [___]        [___]    |
         |                                   |
         |  perspective [___]                |
         |                                   |
         |          [apply] [done] [cancel]  |
         |___________________________________|

***/

   ww = E3pxm->ww;
   hh = E3pxm->hh;

   zdialog *zd = zdialog_new(E2X("Make Waves"),Mwin,Bapply,Bdone,Bcancel,null);
   EFwaves.zd = zd;
   zdialog_add_widget(zd,"hbox","hbw","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbw1","hbw",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbw2","hbw",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbw3","hbw",0,"space=3");
   zdialog_add_widget(zd,"label","space","vbw1"," ","space=1");
   zdialog_add_widget(zd,"label","labwl","vbw1",E2X("wavelength"),"expand");
   zdialog_add_widget(zd,"label","labamp","vbw1",E2X("amplitude"),"expand");
   zdialog_add_widget(zd,"label","labamp","vbw1",E2X("variance"),"expand");
   zdialog_add_widget(zd,"label","labh","vbw2",E2X("horizontal"),"space=1");
   zdialog_add_widget(zd,"zspin","wlh","vbw2","3|500|1|50","expand");
   zdialog_add_widget(zd,"zspin","amph","vbw2","0|100|1|20","expand");
   zdialog_add_widget(zd,"zspin","varh","vbw2","0|100|1|20","expand");
   zdialog_add_widget(zd,"label","labh","vbw3",E2X("vertical"),"space=1");
   zdialog_add_widget(zd,"zspin","wlv","vbw3","3|500|1|50","expand");
   zdialog_add_widget(zd,"zspin","ampv","vbw3","0|100|1|20","expand");
   zdialog_add_widget(zd,"zspin","varv","vbw3","0|100|1|20","expand");
   zdialog_add_widget(zd,"hsep","sepp","dialog",0,"space=3");
   zdialog_add_widget(zd,"hbox","hbp","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labp","hbp",E2X("perspective"),"space=3");
   zdialog_add_widget(zd,"zspin","persp","hbp","0|100|1|0","space=5");

   zdialog_restore_inputs(zd);                                                   //  restore previous inputs
   zdialog_run(zd,waves_dialog_event,"save");                                    //  run dialog - parallel
   return;
}


//  waves dialog event and completion function

int waves_dialog_event(zdialog *zd, const char *event)                           //  waves dialog event function
{
   using namespace waves_names;

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  apply
         zd->zstat = 0;                                                          //  keep dialog active
         edit_reset();
         zdialog_fetch(zd,"wlv",WLV);                                            //  get user inputs
         zdialog_fetch(zd,"wlh",WLH);
         zdialog_fetch(zd,"ampv",AMPV);
         zdialog_fetch(zd,"amph",AMPH);
         zdialog_fetch(zd,"varv",VARV);
         zdialog_fetch(zd,"varh",VARH);
         zdialog_fetch(zd,"persp",PERSP);
         signal_thread();                                                        //  calculate
         return 1;
      }
      if (zd->zstat == 2) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
   }

   return 1;
}


//  thread function - multiple working threads to update image

void * waves_thread(void *)
{
   using namespace waves_names;

   void * waves_wthread(void *);

   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      do_wthreads(waves_wthread,NWT);                                            //  worker threads

      CEF->Fmods++;                                                              //  image modified
      CEF->Fsaved = 0;                                                           //  not saved

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop warning
}


void * waves_wthread(void *arg)                                                  //  working threads
{
   using namespace waves_names;

   int      index = *((int *) (arg));
   float    wlv, wlh, ampv, amph, varv, varh, persp;
   int      py, px, pylo, pyhi;
   int      ii, edist = 0;
   float    dx, dy, *pix1, *pix3, vpix[4];
   float    f1, f2, ffv, ffh, red, green, blue;

   wlv = WLV;                                                                    //  vertical and horizontal wavelengths
   wlh = WLH;
   ampv = 0.01 * AMPV * wlv;                                                     //  vertical and horizoneal amplitudes
   amph = 0.01 * AMPH * wlh;
   varv = 0.01 * VARV;                                                           //  vertical and horizontal variance
   varh = 0.01 * VARH;
   persp = 0.01 * PERSP;                                                         //  perspective 0 to 1.0

   pylo = 0;                                                                     //  py range
   pyhi = hh;

   if (sa_stat == 3) {
      pylo = sa_miny - wlv;                                                      //  rescale to select area height
      if (pylo < 0) pylo = 0;
      pyhi = sa_maxy + wlv;
      if (pyhi > hh) pyhi = hh;
   }

   for (py = pylo + index; py < pyhi; py += NWT)
   {
      if (persp == 0) ffv = ffh = 1.0;
      else {
         ffv = 2.0 * persp * (py - pylo) / (pyhi - pylo);                        //  0 ... 2.0
         ffv = 1.0 + ffv * ffv;                                                  //  1 ... 5.0
         ffh = sqrtf(ffv);                                                       //  1 ... 2.23
      }

      dx = ampv * sin(2.0 * PI * py / (ffv * wlv));

      if (varv > 0)
         dx += ampv * varv * sin(2.0 * PI * py / (1.37 * ffv * wlv));            //  make irregular wave

      for (px = 0; px < ww; px++)
      {
         if (sa_stat == 3) {                                                     //  select area active
            ii = py * ww + px;
            edist = sa_pixmap[ii];                                               //  distance from edge
            if (! edist) continue;                                               //  pixel outside area
         }

         dy = amph * sin(2.0 * PI * px / (ffh * wlh));

         if (varh > 0)
            dy += amph * varh * sin(2.0 * PI * px / (1.37 * ffh * wlh));         //  make irregular wave

         vpixel(E1pxm,px+dx,py+dy,vpix);                                         //  source pixel, displaced
         red = vpix[0];
         green = vpix[1];
         blue = vpix[2];

         if (sa_stat == 3 && edist < sa_blendwidth) {                            //  blend edges
            f1 = sa_blendfunc(edist);
            f2 = 1.0 - f1;
            pix1 = PXMpix(E1pxm,px,py);
            red = f1 * red + f2 * pix1[0];
            green = f1 * green + f2 * pix1[1];
            blue = f1 * blue + f2 * pix1[2];
         }

         pix3 = PXMpix(E3pxm,px,py);                                             //  destination pixel
         pix3[0] = red;
         pix3[1] = green;
         pix3[2] = blue;
      }
   }

   pthread_exit(0);                                                              //  exit thread
}


/********************************************************************************/

//  Twist the image with max. rotation at mouse center and none at farthest edge

namespace twist_names
{
   editfunc    EFtwist;
   int         cx, cy;
   int         ww, hh;
   float       twist;
   float       center;
   float       rotate;
}


//  menu function

void m_twist(GtkWidget *, cchar *)
{
   using namespace twist_names;

   int    twist_dialog_event(zdialog *zd, cchar *event);
   void * twist_thread(void *);
   void   twist_mousefunc();

   F1_help_topic = "twist";

   cchar    *title = E2X("Twist");

   m_zoom(0,"fit");                                                              //  zoom to fit window

   EFtwist.menufunc = m_twist;
   EFtwist.funcname = "twist";
   EFtwist.FprevReq = 1;                                                         //  use preview image
   EFtwist.threadfunc = twist_thread;                                            //  thread function
   EFtwist.mousefunc = twist_mousefunc;                                          //  mouse function
   if (! edit_setup(EFtwist)) return;                                            //  setup edit

   PXM_addalpha(E0pxm);                                                          //  add alpha channel if missing
   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

/***
          _________________________________________
         |              Twist                      |
         |                                         |
         |  Drag mouse to set center               |
         |                                         |
         |   Twist  ==============[]=============  |
         |   Center ==========[]=================  |
         |   Rotate ======[]=====================  |
         |                                         |
         |                 [Reset] [Done] [Cancel] |
         |_________________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,Breset,Bdone,Bcancel,null);
   EFtwist.zd = zd;
   
   zdialog_add_widget(zd,"label","labtip","dialog",E2X("Drag mouse to set center"));

   zdialog_add_widget(zd,"hbox","hba","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","laba","hba",E2X("Twist"),"space=3");
   zdialog_add_widget(zd,"hscale","twist","hba","-1.0|+1.0|0.01|0.0","space=5|expand");

   zdialog_add_widget(zd,"hbox","hbp","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labp","hbp",Bcenter,"space=3");
   zdialog_add_widget(zd,"hscale","center","hbp","1.0|3.0|0.1|1.0","space=5|expand");

   zdialog_add_widget(zd,"hbox","hbc","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labc","hbc",Brotate,"space=3");
   zdialog_add_widget(zd,"hscale","rotate","hbc","-4.0|4.0|0.01|0.0","space=5|expand");
   
   ww = E3pxm->ww;
   hh = E3pxm->hh;

   cx = ww / 2;                                                                  //  initial data
   cy = hh / 2;
   twist = 0.0;
   center = 1.0;
   rotate = 0.0;
   
   signal_thread();

   zdialog_resize(zd,300,0);
   zdialog_run(zd,twist_dialog_event,"save");                                    //  run dialog - parallel

   takeMouse(twist_mousefunc,dragcursor);                                        //  connect mouse function
   return;
}


//  dialog event and completion callback function

int twist_dialog_event(zdialog *zd, cchar *event)
{
   using namespace twist_names;

   void  twist_mousefunc();

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()

   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();
      signal_thread();
      return 1;
   }

   if (zd->zstat)
   {  
      if (zd->zstat == 1) {                                                      //  reset
         zd->zstat = 0;
         twist = 0.0;
         center = 1.0;
         rotate = 0.0;
         zdialog_stuff(zd,"twist",0.0);
         zdialog_stuff(zd,"center",1.0);
         zdialog_stuff(zd,"rotate",0.0);
         signal_thread();
      }
      else if (zd->zstat == 2) {                                                 //  done
         wait_thread_idle();                                                     //  insure thread done
         float R = 1.0 * (E0pxm->ww + E0pxm->hh) / (E3pxm->ww + E3pxm->hh);
         cx = R * cx;                                                            //  scale geometries to full size
         cy = R * cy;
         edit_fullsize();                                                        //  get full size image
         signal_thread();
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   if (strmatch(event,"focus"))                                                  //  toggle mouse capture
      takeMouse(twist_mousefunc,dragcursor);                                     //  connect mouse function

   if (strmatch(event,"blendwidth")) signal_thread();

   if (strmatch(event,"twist")) {
      zdialog_fetch(zd,"twist",twist);
      signal_thread();
   }

   if (strmatch(event,"center")) {
      zdialog_fetch(zd,"center",center);
      signal_thread();
   }

   if (strmatch(event,"rotate")) {
      zdialog_fetch(zd,"rotate",rotate);
      signal_thread();
   }

   return 1;
}


//  get mouse position and set new center for twist

void twist_mousefunc()
{
   using namespace twist_names;

   if (! LMclick && ! Mdrag) return;

   cx = Mxposn;                                                                  //  new twist center = mouse position
   cy = Myposn;
   signal_thread();                                                              //  trigger image update

   LMclick = Mxdrag = Mydrag = 0;
   return;
}


//  thread function

void * twist_thread(void *)
{
   using namespace twist_names;

   void * twist_wthread(void *arg);

   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      PXM_clear(E3pxm,0);                                                        //  clear E3 to black/transparent

      do_wthreads(twist_wthread,NWT);                                            //  worker threads

      CEF->Fmods++;
      CEF->Fsaved = 0;

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();
   }

   return 0;                                                                     //  not executed, stop g++ warning
}


//  working thread

void * twist_wthread(void *arg)
{
   using namespace twist_names;

   int         index, vstat;
   int         px3, py3, px33, py33;
   float       qx, qy;
   float       vpix[4], *pix3;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);
   int         Dx, Dy, Dnw, Dne, Dse, Dsw;
   float       D, DN, Dmax;
   float       T, Tp, Tq, Tmax;
   
   ww = E3pxm->ww;
   hh = E3pxm->hh;
   
   Dnw = cx * cx + cy * cy;                                                      //  get distance from center
   Dne = (ww-cx) * (ww-cx) + cy * cy;                                            //    to nearestest corner
   Dse = (ww-cx) * (ww-cx) + (hh-cy) * (hh-cy);
   Dsw = cx * cx + (hh-cy) * (hh - cy);
   Dmax = Dnw;
   if (Dne > Dmax) Dmax = Dne;
   if (Dse > Dmax) Dmax = Dse;
   if (Dsw > Dmax) Dmax = Dsw;

   Dmax = sqrtf(Dmax);
   
   Tmax = 6 * PI * twist;                                                        //  twist angle

   index = *((int *) arg);

   for (py3 = index; py3 < hh; py3 += NWT)                                       //  loop all output pixels
   for (px3 = 0; px3 < ww; px3++)
   {
      Dx = px3 - cx;                                                             //  px/py relative to cx/cy
      Dy = py3 - cy;

      D = Dx * Dx + Dy * Dy;                                                     //  distance pixel to center
      if (D == 0) continue;
      D = sqrtf(D);
      
      Tp = asinf(Dy/D);                                                          //  angle of pixel line to center
      if (Dx < 0) {
         if (Dy > 0) Tp = PI - Tp;
         else Tp = - PI - Tp;
      }

      DN = pow((D/Dmax),center);                                                 //  distance normalized, 0.0 ... 1.0

      T = Tmax * DN;                                                             //  rotation at distance from center
      T = T + rotate;

      Tq = Tp + T;                                                               //  rotated pixel angle
      
      qx = D * cosf(Tq);                                                         //  rotated pixel position
      qy = D * sinf(Tq);
      qx = 2.0 * qx + cx;                                                        //  19.0
      qy = 2.0 * qy + cy;
      vstat = vpixel(E1pxm,qx,qy,vpix);                                          //  input pixel
      
      px33 = px3 - (cx - ww/2);                                                  //  move output image to center
      py33 = py3 - (cy - hh/2);
      if (px33 < 0 || px33 > ww-1) continue;
      if (py33 < 0 || py33 > hh-1) continue;
      pix3 = PXMpix(E3pxm,px33,py33);                                            //  output pixel

      if (vstat) memcpy(pix3,vpix,pcc);
      else memset(pix3,0,pcc);
   }

   pthread_exit(0);
}


/********************************************************************************/

//  project image on to a sphere with adjustable radius (flatness)
//  image shrinks with increasing radius

namespace sphere_names
{
   int         E3ww, E3hh;                                                       //  image dimensions
   int         Xcen, Ycen, D1;                                                   //  center and diameter of sphere
   float       flatten;                                                          //  flatten parameter
   float       magnify;                                                          //  magnify parameter
   editfunc    EFsphere;                                                         //  edit function data
}


//  menu function

void m_sphere(GtkWidget *, const char *)
{
   using namespace sphere_names;

   int    sphere_dialog_event(zdialog* zd, const char *event);
   void   sphere_mousefunc(void);
   void * sphere_thread(void *);
   
   cchar  *title = E2X("Spherical Projection");

   F1_help_topic = "sphere";

   m_zoom(0,"fit");                                                              //  zoom to fit window

   EFsphere.menufunc = m_sphere;
   EFsphere.funcname = "sphere";                                                 //  function name
   EFsphere.FprevReq = 1;                                                        //  use preview edit mode
   EFsphere.mousefunc = sphere_mousefunc;                                        //  mouse function
   EFsphere.threadfunc = sphere_thread;                                          //  thread function
   if (! edit_setup(EFsphere)) return;                                           //  setup edit

   PXM_addalpha(E0pxm);                                                          //  add alpha channel if missing
   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

   E3ww = E3pxm->ww;
   E3hh = E3pxm->hh;
   
   Xcen = E3ww / 2;                                                              //  set dialog defaults
   Ycen = E3hh / 2;
   D1 = E3ww;
   if (E3hh < E3ww) D1 = E3hh;
   flatten = 0;
   magnify = 1.0;

/***
       _____________________________
      |    Spherical Projection     |
      |                             |
      | Drag mouse to set center    |
      |                             |
      | Flatten ======[]==========  |
      | Magnify ==========[]======  |
      |                             |
      |            [done] [cancel]  |
      |_____________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,Bdone,Bcancel,null);                     //  sphere dialog
   CEF->zd = zd;

   zdialog_add_widget(zd,"label","labtip","dialog",E2X("Drag mouse to set center"));
   zdialog_add_widget(zd,"hbox","hbflat","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labflat","hbflat",Bflatten,"space=5");
   zdialog_add_widget(zd,"hscale","flatten","hbflat","0.0|0.999|0.001|0.0","expand");
   zdialog_add_widget(zd,"hbox","hbmag","dialog");
   zdialog_add_widget(zd,"label","labmag","hbmag",Bmagnify,"space=5");
   zdialog_add_widget(zd,"hscale","magnify","hbmag","1.0|2.0|0.001|1.0","expand");

   zdialog_resize(zd,250,0);
   zdialog_run(zd,sphere_dialog_event,"save");                                   //  run dialog - parallel
   takeMouse(sphere_mousefunc,0);                                                //  connect mouse function
   signal_thread();                                                              //  trigger update thread
   return;
}


//  sphere dialog event and completion function

int sphere_dialog_event(zdialog *zd, const char *event)
{
   using namespace sphere_names;
   
   float    scale;

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  done
      {
         edit_fullsize();                                                        //  get full size image
         scale = 1.0 * (E3pxm->ww + E3pxm->hh) / (E3ww + E3hh);                  //  scale up the parameters
         E3ww = E3pxm->ww;
         E3hh = E3pxm->hh;
         Xcen = scale * Xcen;
         Ycen = scale * Ycen;
         D1 = scale * D1;
         signal_thread();                                                        //  recalculate image
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }
   
   if (strmatch(event,"flatten")) {
      zdialog_fetch(zd,"flatten",flatten);
      signal_thread();                                                           //  trigger update thread
   }

   if (strmatch(event,"magnify")) {
      zdialog_fetch(zd,"magnify",magnify);
      signal_thread();                                                           //  trigger update thread
   }

   return 1;
}


//  mouse function - get new center from mouse click or drag

void sphere_mousefunc()
{
   using namespace sphere_names;
   
   int      edist;
   
   if (! LMclick && ! Mdrag) return;
   LMclick = Mxdrag = Mydrag = 0;
   
   if (Mxposn < 0.1 * E3ww || Mxposn > 0.9 * E3ww) return;                       //  ignore if near image edge
   if (Myposn < 0.1 * E3hh || Myposn > 0.9 * E3hh) return;

   Xcen = Mxposn;                                                                //  new center
   Ycen = Myposn;
   
   edist = Xcen;                                                                 //  find nearest edge distance
   if (Ycen < edist) edist = Ycen;
   if (E3ww - Xcen < edist) edist = E3ww - Xcen;
   if (E3hh - Ycen < edist) edist = E3hh - Ycen;
   
   D1 = 2 * edist;                                                               //  new sphere diameter
   signal_thread();

   return;
}


//  thread function - multiple working threads to update image

void * sphere_thread(void *)
{
   using namespace sphere_names;

   void  * sphere_wthread(void *arg);                                            //  worker thread
   
   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      PXM_clear(E3pxm,0);                                                        //  clear E3 to black/transparent

      do_wthreads(sphere_wthread,NWT);                                           //  worker threads

      CEF->Fmods++;                                                              //  image modified
      CEF->Fsaved = 0;                                                           //  not saved

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop warning
}


void * sphere_wthread(void *arg)                                                 //  worker thread function
{
   using namespace sphere_names;

   int         index = *((int *) (arg));
   int         px3, py3, px33, py33, dx, dy, vstat;
   float       px1, py1, *pix3, vpix[4];
   float       s1, s2, D2, T;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);
   
   D2 = D1 / (1.0 - flatten);                                                    //  image diameter
   
   for (py3 = index; py3 < E3hh; py3 += NWT)                                     //  loop output pixels
   for (px3 = 0; px3 < E3ww; px3++)
   {
      dx = px3 - Xcen;                                                           //  distance from mouse center
      dy = py3 - Ycen;
      s1 = sqrtf(dx*dx + dy*dy);                                                 //  dist. from center to output pixel
      T = s1 * PI / D2 / magnify;                                                //  sine of subtended angle
      if (T > 1.0) continue;
      T = asinf(T);                                                              //  subtended angle, 0 - PI/2

      s2 = D2 * T / PI;                                                          //  corresp. dist. on sphere
      px1 = Xcen + dx * s2 / s1;                                                 //  input virtual pixel
      py1 = Ycen + dy * s2 / s1;
      vstat = vpixel(E1pxm,px1,py1,vpix);
      if (! vstat) continue;

      px33 = px3 - (Xcen - E3ww/2);                                              //  move to image center
      py33 = py3 - (Ycen - E3hh/2);
      if (px33 < 0 || px33 > E3ww-1) continue;
      if (py33 < 0 || py33 > E3hh-1) continue;
      pix3 = PXMpix(E3pxm,px33,py33);                                            //  output pixel
      memcpy(pix3,vpix,pcc);
   }

   pthread_exit(0);                                                              //  exit thread
}


/********************************************************************************/

//  Stretch image.
//  Image expands with increasing radius.

namespace stretch_names
{
   int         E3ww, E3hh;                                                       //  image dimensions
   int         Xcen, Ycen, D1;                                                   //  center and diameter of circle
   float       magnify;                                                          //  magnify parameter
   float       flatten;                                                          //  flatten parameter
   editfunc    EFstretch;                                                        //  edit function data
}


//  menu function

void m_stretch(GtkWidget *, const char *)                                        //  19.0
{
   using namespace stretch_names;

   int    stretch_dialog_event(zdialog* zd, const char *event);
   void   stretch_mousefunc(void);
   void * stretch_thread(void *);
   
   cchar  *title = E2X("Stretch Image");

   F1_help_topic = "stretch";

   m_zoom(0,"fit");                                                              //  zoom to fit window

   EFstretch.menufunc = m_stretch;
   EFstretch.funcname = "stretch";                                               //  function name
   EFstretch.FprevReq = 1;                                                       //  use preview edit mode
   EFstretch.mousefunc = stretch_mousefunc;                                      //  mouse function
   EFstretch.threadfunc = stretch_thread;                                        //  thread function
   if (! edit_setup(EFstretch)) return;                                          //  setup edit

   PXM_addalpha(E0pxm);                                                          //  add alpha channel if missing
   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

   E3ww = E3pxm->ww;
   E3hh = E3pxm->hh;
   
   Xcen = E3ww / 2;                                                              //  set dialog defaults
   Ycen = E3hh / 2;
   D1 = E3ww;
   if (E3hh < E3ww) D1 = E3hh;
   magnify = 1.0;

/***
       _____________________________
      |      Stretch Image          |
      |                             |
      | Drag mouse to set center    |
      |                             |
      | Flatten ======[]==========  |
      | Magnify ==========[]======  |
      |                             |
      |            [done] [cancel]  |
      |_____________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,Bdone,Bcancel,null);                     //  stretch dialog
   CEF->zd = zd;

   zdialog_add_widget(zd,"label","labtip","dialog",E2X("Drag mouse to set center"));
   zdialog_add_widget(zd,"hbox","hbflat","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labflat","hbflat",Bflatten,"space=5");
   zdialog_add_widget(zd,"hscale","flatten","hbflat","0.0|1.49|0.001|0.0","expand");
   zdialog_add_widget(zd,"hbox","hbmag","dialog");
   zdialog_add_widget(zd,"label","labmag","hbmag",Bmagnify,"space=5");
   zdialog_add_widget(zd,"hscale","magnify","hbmag","1.0|2.0|0.001|1.0","expand");

   zdialog_resize(zd,250,0);
   zdialog_run(zd,stretch_dialog_event,"save");                                  //  run dialog - parallel
   takeMouse(stretch_mousefunc,0);                                               //  connect mouse function
   signal_thread();                                                              //  trigger update thread
   return;
}


//  stretch dialog event and completion function

int stretch_dialog_event(zdialog *zd, const char *event)
{
   using namespace stretch_names;
   
   float    scale;

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  done
      {
         edit_fullsize();                                                        //  get full size image
         scale = 1.0 * (E3pxm->ww + E3pxm->hh) / (E3ww + E3hh);                  //  scale up the parameters
         E3ww = E3pxm->ww;
         E3hh = E3pxm->hh;
         Xcen = scale * Xcen;
         Ycen = scale * Ycen;
         D1 = scale * D1;
         signal_thread();                                                        //  recalculate image
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }
   
   if (strmatch(event,"flatten")) {
      zdialog_fetch(zd,"flatten",flatten);
      signal_thread();                                                           //  trigger update thread
   }

   if (strmatch(event,"magnify")) {
      zdialog_fetch(zd,"magnify",magnify);
      signal_thread();                                                           //  trigger update thread
   }

   return 1;
}


//  mouse function - get new center from mouse click or drag

void stretch_mousefunc()
{
   using namespace stretch_names;
   
   int      edist;
   
   if (! LMclick && ! Mdrag) return;
   LMclick = Mxdrag = Mydrag = 0;
   
   if (Mxposn < 0.1 * E3ww || Mxposn > 0.9 * E3ww) return;                       //  ignore if near image edge
   if (Myposn < 0.1 * E3hh || Myposn > 0.9 * E3hh) return;

   Xcen = Mxposn;                                                                //  new center
   Ycen = Myposn;
   
   edist = Xcen;                                                                 //  find nearest edge distance
   if (Ycen < edist) edist = Ycen;
   if (E3ww - Xcen < edist) edist = E3ww - Xcen;
   if (E3hh - Ycen < edist) edist = E3hh - Ycen;
   
   D1 = 2 * edist;                                                               //  new stretch diameter
   signal_thread();

   return;
}


//  thread function - multiple working threads to update image

void * stretch_thread(void *)
{
   using namespace stretch_names;

   void  * stretch_wthread(void *arg);                                           //  worker thread
   
   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      PXM_clear(E3pxm,0);                                                        //  clear E3 to black/transparent

      do_wthreads(stretch_wthread,NWT);                                          //  worker threads

      CEF->Fmods++;                                                              //  image modified
      CEF->Fsaved = 0;                                                           //  not saved

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop warning
}


void * stretch_wthread(void *arg)                                                //  worker thread function
{
   using namespace stretch_names;

   int         index = *((int *) (arg));
   int         px3, py3, px33, py33, dx, dy, vstat;
   float       px1, py1, *pix3, vpix[4];
   float       s1, s2, D2, R, T;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   D2 = D1 / (1.5 - flatten);                                                    //  diameter of output image
   R = D2 / 2.0;                                                                 //  radius
   
   for (py3 = index; py3 < E3hh; py3 += NWT)                                     //  loop output pixels
   for (px3 = 0; px3 < E3ww; px3++)
   {
      dx = px3 - Xcen;                                                           //  distance from mouse center
      dy = py3 - Ycen;
      s1 = sqrtf(dx*dx + dy*dy);                                                 //  center to output pixel
      T = s1 / R;                                                                //  subtended angle
      if (T > 0.5 * PI) continue;                                                //  limit 90 deg.
      s2 = R * sinf(T) * (3 - magnify);                                          //  center to input pixel
      
      px1 = Xcen + dx * s2 / s1;                                                 //  input v.pixel
      py1 = Ycen + dy * s2 / s1;
      vstat = vpixel(E1pxm,px1,py1,vpix);
      if (! vstat) continue;

      px33 = px3 - (Xcen - E3ww/2);                                              //  move to image center
      py33 = py3 - (Ycen - E3hh/2);
      if (px33 < 0 || px33 > E3ww-1) continue;
      if (py33 < 0 || py33 > E3hh-1) continue;
      pix3 = PXMpix(E3pxm,px33,py33);                                            //  output pixel
      memcpy(pix3,vpix,pcc);
   }

   pthread_exit(0);                                                              //  exit thread
}


/********************************************************************************/

//  Turn an image inside-out.
//  Convert pixels within the largest circle fitting within the image.
//  Replace each pixel having distance R from center with a pixel on the
//  same center to edge line having distance R from edge of circle.

namespace inside_out_names
{
   editfunc    EFinsideout;
   int         ww, hh, cx, cy, pixcc;
   float       R, T, hole;                                                       //  radius, theta
}


//  menu function

void m_inside_out(GtkWidget *, cchar *menu)                                      //  19.0
{
   using namespace inside_out_names;
   
   int    inside_out_dialog_event(zdialog *zd, cchar *event);
   void * inside_out_thread(void *);
   void   inside_out_mousefunc();
   
   F1_help_topic = "inside-out";

   m_zoom(0,"fit");                                                              //  zoom to fit window

   EFinsideout.menuname = menu;
   EFinsideout.menufunc = m_inside_out;
   EFinsideout.funcname = "inside_out";                                          //  function name
   EFinsideout.FprevReq = 1;                                                     //  use preview image
   EFinsideout.threadfunc = inside_out_thread;                                   //  thread function
   EFinsideout.mousefunc = inside_out_mousefunc;                                 //  mouse function

   if (! edit_setup(EFinsideout)) return;                                        //  open edit function

   PXM_addalpha(E0pxm);                                                          //  add an alpha channel if missing
   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

   ww = E3pxm->ww;                                                               //  image dimensions
   hh = E3pxm->hh;
   cx = ww / 2;
   cy = hh / 2;
   T = 0.0;
   hole = 100;

   pixcc = E3pxm->nc * sizeof(float);                                            //  RGBA pixel size

/***
          ________________________________
         |         Inside-out             |
         |  Drag mouse to set center      |
         |                                |
         |  Rotate ===========[]========= |
         |   Hole ======[]=============== |
         |                                |
         |                [Done] [Cancel] |
         |________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Inside-out"),Mwin,Bdone,Bcancel,null);
   EFinsideout.zd = zd;

   zdialog_add_widget(zd,"label","labtip","dialog",E2X("Drag mouse to set center"));
   zdialog_add_widget(zd,"hbox","hbrot","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labrot","hbrot",Brotate,"space=3");
   zdialog_add_widget(zd,"hscale","rotate","hbrot","-4.0|4.0|0.01|0.0","space=5|expand");
   zdialog_add_widget(zd,"hbox","hbhole","dialog");
   zdialog_add_widget(zd,"label","labhole","hbhole",E2X("Center Hole"),"space=3");
   zdialog_add_widget(zd,"hscale","hole","hbhole","0|1000|1|100","space=5|expand");

   takeMouse(inside_out_mousefunc,dragcursor);
   zdialog_run(zd,inside_out_dialog_event,"save");
   signal_thread();
   return;
}


//  dialog event and completion callback function

int inside_out_dialog_event(zdialog *zd, cchar *event)
{
   using namespace inside_out_names;

   void   inside_out_mousefunc();

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()

   if (strmatch(event,"focus"))                                                  //  toggle mouse capture
      takeMouse(inside_out_mousefunc,dragcursor);                                //  connect mouse function

   if (strmatch(event,"rotate")) {
      zdialog_fetch(zd,"rotate",T);
      signal_thread();
   }

   if (strmatch(event,"hole")) {
      zdialog_fetch(zd,"hole",hole);
      signal_thread();
   }
      
   if (zd->zstat)
   {  
      if (zd->zstat == 1) {                                                      //  done
         wait_thread_idle();                                                     //  insure thread done
         float S = 1.0 * E0pxm->ww / E3pxm->ww;
         cx = S * cx;                                                            //  scale params to full size
         cy = S * cy;
         hole = S * hole;
         edit_fullsize();                                                        //  get full size image
         signal_thread();
         edit_done(0);                                                           //  commit edit
      }

      else edit_cancel(0);                                                       //  discard edit
   }

   return 1;
}


//  mouse function

void inside_out_mousefunc()
{
   using namespace inside_out_names;

   if (! LMclick && ! Mdrag) return;                                             //  wait for click or drag
   cx = Mxposn;                                                                  //  mouse position = new center
   cy = Myposn;
   signal_thread();                                                              //  convert image

   LMclick = Mxdrag = Mydrag = 0;
   return;
}


//  main thread function

void * inside_out_thread(void *)
{
   using namespace inside_out_names;

   void * inside_out_wthread(void *arg);
   
   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request
      
      ww = E3pxm->ww;                                                            //  image dimensions
      hh = E3pxm->hh;                                                            //  (preview or full size)
      
      R = cx;                                                                    //  R = minimum center - edge distance
      if (cy < cx) R = cy;
      if (ww - cx < R) R = ww - cx;
      if (hh - cy < R) R = hh - cy;

      if (R < 50) continue;                                                      //  ignore absurd center
      
      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);
      
      PXM_clear(E3pxm,0);                                                        //  clear E3 to black/transparent

      do_wthreads(inside_out_wthread,NWT);                                       //  do worker threads

      paintlock(0);                                                              //  unblock window paint

      CEF->Fmods++;                                                              //  E3 modified, not saved
      CEF->Fsaved = 0;

      Fpaint2();                                                                 //  update output image
   }

   return 0;                                                                     //  not executed, stop g++ warning
}


//  worker thread function

void * inside_out_wthread(void *arg)
{
   using namespace inside_out_names;

   int      index = *((int *) arg);
   float    px1, py1, px3, py3, px33, py33;
   float    dx1, dy1, dx3, dy3, r1, r3, T1, T3;
   float    pix1[4], *pix3;


   for (py3 = index; py3 < hh; py3 += NWT)                                       //  loop target pixels
   for (px3 = 0; px3 < ww; px3++)
   {
      px33 = px3 - (cx - ww/2);                                                  //  move to image center
      py33 = py3 - (cy - hh/2);

      if (px33 < 0 || px33 > ww-1) continue;
      if (py33 < 0 || py33 > hh-1) continue;
   
      dx3 = px3 - cx;                                                            //  target pixel relative to center
      dy3 = py3 - cy;
      r3 = sqrtf(dx3*dx3 + dy3*dy3);                                             //  target pixel distance from center

      if (r3 < hole) continue;
      if (r3 > R) continue;                                                      //  outside image circle
      
      T3 = asinf(dy3 / r3);                                                      //  target pixel angle from center
      if (dx3 < 0) T3 = PI - T3;
      if (T3 > PI) T3 -= 2 * PI;
      if (T3 < PI) T3 += 2 * PI;
      
      T1 = T3 + T;                                                               //  source pixel angle from center
      if (T1 > PI) T1 -= 2 * PI;                                                 //    + user rotate
      if (T1 < PI) T1 += 2 * PI;

      r1 = R * (1 + (r3 - hole) / (hole - R));                                   //  r3: hole >> R   r1: R >> 0

      dx1 = r1 * cosf(T1);                                                       //  source pixel relative to center
      dy1 = r1 * sinf(T1);

      px1 = dx1 + cx;                                                            //  add center
      py1 = dy1 + cy;
      
      if (! vpixel(E1pxm,px1,py1,pix1)) continue;                                //  get source pixel

      pix3 = PXMpix(E3pxm,int(px33),int(py33));                                  //  output pixel
      if (px33 < 0 || px33 > ww-1) continue;
      if (py33 < 0 || py33 > hh-1) continue;

      memcpy(pix3,pix1,pixcc);
   }

   pthread_exit(0);
}


/********************************************************************************/

//  Convert image to 'tiny planet' format.

namespace tiny_planet_names
{
   editfunc    EFtinyplanet;

   int      ww1, hh1, ww3, hh3, ww9, hh9;
   int      cx9, cy9, cx3, cy3, pixcc;
   int      cuttop, cutbott, rotate;
   float    hole, R1, R2;
   int      revR, revT;
}


//  menu function

void m_tiny_planet(GtkWidget *, cchar *menu)                                     //  19.0
{
   using namespace tiny_planet_names;
   
   int   tiny_planet_dialog_event(zdialog *zd, cchar *event);
   void  tiny_planet_process();
   
   char  texthole[20], texttop[20], textbott[20];
   
   F1_help_topic = "tiny planet";
   
   if (! curr_file) return;                                                      //  no current image

   if (Fpxb->ww < Fpxb->hh) {
      zmessageACK(Mwin,E2X("image width must be greater than height"));
      return;
   }

   EFtinyplanet.menuname = menu;
   EFtinyplanet.menufunc = m_tiny_planet;
   EFtinyplanet.funcname = "tiny_planet";                                        //  function name
   EFtinyplanet.FprevReq = 1;                                                    //  use preview image

   if (! edit_setup(EFtinyplanet)) return;                                       //  open edit function

   PXM_addalpha(E0pxm);                                                          //  add alpha channel if missing
   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

   ww1 = E1pxm->ww;                                                              //  image dimensions
   hh1 = E1pxm->hh;

   pixcc = 4 * sizeof(float);                                                    //  RGBA pixel size

/***
          ___________________________________
         |         Tiny Planet               |
         |                                   |
         | Center Hole ==========[]========= |
         | Cut Top ======[]================= |
         | Cut Bottom =[]=================== |
         | Rotate =========[]=============== |
         | [ ] Reverse R   [ ] Theta         |
         |                                   |
         |           [Reset] [Done] [Cancel] |
         |___________________________________|


        hh is image height in pixels

              blank         image area       blank
          |-------------|-----------------|---------
        center          R1                R2
                       < 0.4*hh           0.5*hh
                       = hole             fixed
***/

   zdialog *zd = zdialog_new("Tiny Planet",Mwin,Bdone,Bcancel,null);
   EFtinyplanet.zd = zd;

   snprintf(texthole,20,"0|%.0f|1|%.0f", 0.4 * hh1, 0.1 * hh1);                  //  hole range and default
   zdialog_add_widget(zd,"hbox","hbhole","dialog");
   zdialog_add_widget(zd,"label","labhole","hbhole",E2X("Center Hole"),"space=3");
   zdialog_add_widget(zd,"hscale","hole","hbhole",texthole,"space=3|expand");

   snprintf(texttop,20,"0|%.0f|1|0", 0.3 * hh1);                                 //  cut-off top range
   zdialog_add_widget(zd,"hbox","hbtop","dialog");
   zdialog_add_widget(zd,"label","labcuttop","hbtop",E2X("Cut Top"),"space=3");
   zdialog_add_widget(zd,"hscale","cuttop","hbtop",texttop,"space=3|expand");

   snprintf(textbott,20,"0|%.0f|1|0", 0.3 * hh1);                                //  cut-off bottom range
   zdialog_add_widget(zd,"hbox","hbbott","dialog");
   zdialog_add_widget(zd,"label","labcutbott","hbbott",E2X("Cut Bottom"),"space=3");
   zdialog_add_widget(zd,"hscale","cutbott","hbbott",textbott,"space=3|expand");
   
   zdialog_add_widget(zd,"hbox","hbrotate","dialog");                            //  rotate range
   zdialog_add_widget(zd,"label","labrotate","hbrotate",Brotate,"space=3");
   zdialog_add_widget(zd,"hscale","rotate","hbrotate","0|360|1|0","space=3|expand");

   zdialog_add_widget(zd,"hbox","hbrev","dialog");
   zdialog_add_widget(zd,"zbutton","revR","hbrev",E2X("Reverse R"),"space=3");
   zdialog_add_widget(zd,"zbutton","revT","hbrev",E2X("Theta"),"space=8");
   
   hole = 0.1 * hh1;                                                             //  default settings
   revR = revT = 0;
   cuttop = cutbott = 0;

   zdialog_run(zd,tiny_planet_dialog_event,"save");

   tiny_planet_process();                                                        //  make initial image
   return;
}


//  dialog event and completion callback function

int tiny_planet_dialog_event(zdialog *zd, cchar *event)
{
   using namespace tiny_planet_names;
   
   void tiny_planet_process();
   
   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()

   if (zd->zstat)
   {  
      if (zd->zstat != 1) edit_cancel(0);                                        //  cancel

      else                                                                       //  done
      {
         float R = 1.0 * E0pxm->ww / ww1;                                        //  scale params to full size
         cuttop = R * cuttop;
         cutbott = R * cutbott;
         hole = R * hole;
         edit_fullsize();                                                        //  get full size input image
         ww1 = E1pxm->ww;
         hh1 = E1pxm->hh;
         tiny_planet_process();                                                  //  make full size output image
         PXM_free(E9pxm);
         edit_done(0);
      }

      return 1;
   }
   
   if (strmatch(event,"focus")) return 1;

   zdialog_fetch(zd,"hole",hole);
   zdialog_fetch(zd,"cuttop",cuttop);                                            //  get dialog inputs
   zdialog_fetch(zd,"cutbott",cutbott);
   zdialog_fetch(zd,"rotate",rotate);
   if (strmatch(event,"revR")) revR = 1 - revR;
   if (strmatch(event,"revT")) revT = 1 - revT;
   zmainloop();

   tiny_planet_process();                                                        //  make image

   return 1;
}

void tiny_planet_process()
{
   using namespace tiny_planet_names;
   
   int      py, cc;
   float    *pix1, *pix9;

   void * tiny_planet_wthread(void *arg);
   
   ww9 = ww1;                                                                    //  make input image with cuttoffs
   hh9 = hh1 - cuttop - cutbott;
   if (E9pxm) PXM_free(E9pxm);
   E9pxm = PXM_make(ww9,hh9,4);

   for (py = 0; py < hh9; py++)
   {
      pix1 = PXMpix(E1pxm,0,py + cuttop);
      pix9 = PXMpix(E9pxm,0,py);
      memcpy(pix9,pix1, ww1 * pixcc);
   }

   ww3 = hh3 = hh9 + 2;                                                          //  make square output image
   PXM_free(E3pxm);
   E3pxm = PXM_make(ww3,hh3,4);
   cc = ww3 * hh3 * pixcc;
   memset(E3pxm->pixels,0,cc);                                                   //  all pixels black and transparent

   cx9 = ww9 / 2;                                                                //  input image center
   cy9 = hh9 / 2;

   cx3 = ww3 / 2;                                                                //  output image center
   cy3 = hh3 / 2;
   if (hole > 0.4 * hh9) hole = 0.4 * hh9;                                       //  sanity limit

   R1 = hole;                                                                    //  image range, R1 to R2
   R2 = hh9 / 2;

   do_wthreads(tiny_planet_wthread,NWT);                                         //  do worker threads

   CEF->Fmods++;                                                                 //  E3 modified, not saved
   CEF->Fsaved = 0;

   Fpaint2();                                                                    //  update output image
   return;
}


//  worker thread function

void * tiny_planet_wthread(void *arg)
{
   using namespace tiny_planet_names;

   int      index = *((int *) arg);
   int      px3, py3, dx, dy;
   float    px9, py9, R, theta, pix9[4], *pix3;
   
   for (py3 = index; py3 < hh3; py3 += NWT)                                      //  loop output pixels
   for (px3 = 0; px3 < ww3; px3++)
   {
      dx = px3 - cx3;            
      dy = py3 - cy3;
      R = sqrtf(dx*dx + dy*dy);                                                  //  pixel distance from center

      if (R < 1) continue;
      if (R > R2) continue;                                                      //  outside R2 circle
      if (R < R1) continue;                                                      //  inside R1 circle
      
      if (dx >= 0 && dy >= 0) theta = asinf(dy/R);                               //  0 to 90 deg.
      else if (dx < 0 && dy >= 0) theta = PI - asinf(dy/R);                      //  90 to 180
      else if (dx < 0 && dy < 0) theta = PI + asinf(-dy/R);                      //  180 to 270
      else theta = 2 * PI - asinf(-dy/R);                                        //  270 to 360
      
      theta = theta + rotate / 57.296;                                           //  apply user rotation
      if (theta < 0) theta = 2 * PI - theta;
      if (theta >= 2 * PI) theta = theta - 2 * PI;

      px9 = ww9 * 0.5 * theta / PI;                                              //  R, theta --> input pixel
      py9 = hh9 * (R - R1) / (R2 - R1);
      
      if (revT) px9 = ww9-1 - px9;                                               //  if reverse theta
      if (revR) py9 = hh9-1 - py9;                                               //  if reverse R

      pix3 = PXMpix(E3pxm,px3,py3);                                              //  interpolate nearest 4 pixels
      if (vpixel(E9pxm,px9,py9,pix9))
         memcpy(pix3,pix9,pixcc);
   }

   pthread_exit(0);
}


/********************************************************************************/

//  generate an image that repeats itself spiraling inward

namespace escher_spiral_names
{
   editfunc    EFescher_spiral;
   int         Eww, Ehh;
   int         cx, cy;
   int         width;
   float       rim;
   float       color[3];
}


//  menu function

void m_escher_spiral(GtkWidget *, cchar *menu)                                   //  20.0
{
   using namespace escher_spiral_names;
   
   int    escher_spiral_dialog_event(zdialog *zd, cchar *event);
   void * escher_spiral_thread(void *);
   void   escher_spiral_mousefunc();
   
   F1_help_topic = "escher spiral";

   m_zoom(0,"fit");                                                              //  zoom to fit window

   EFescher_spiral.menuname = menu;
   EFescher_spiral.menufunc = m_escher_spiral;
   EFescher_spiral.funcname = "escher_spiral";                                   //  function name
   EFescher_spiral.threadfunc = escher_spiral_thread;                            //  thread function
   EFescher_spiral.mousefunc = escher_spiral_mousefunc;                          //  mouse function

   if (! edit_setup(EFescher_spiral)) return;                                    //  open edit function

   Eww = E1pxm->ww;                                                              //  source image dimensions
   Ehh = E1pxm->hh;
   cx = Eww / 2;                                                                 //  initial center
   cy = Ehh / 2;
   
   PXM_subalpha(E3pxm);                                                          //  remove dest. image alpha channel

/***
          ________________________________
         |         Escher Spiral          |
         |                                |
         |  Click mouse to change center  |
         |  Width % [ 40 ]                |
         |  Rim % [ 1 ]   Color [ ### ]   |
         |                                |
         |        [Apply] [Done] [Cancel] |
         |________________________________|

***/

   zdialog *zd = zdialog_new("Escher Spiral",Mwin,Bapply,Bdone,Bcancel,null);
   EFescher_spiral.zd = zd;

   zdialog_add_widget(zd,"label","labtip","dialog",E2X("Click mouse to change center"));
   zdialog_add_widget(zd,"hbox","hbw","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labw","hbw",E2X("Width %"),"space=3");
   zdialog_add_widget(zd,"zspin","width","hbw","10|80|1|40","space=5");
   zdialog_add_widget(zd,"hbox","hbr","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labr","hbr",E2X("Rim %"),"space=3");
   zdialog_add_widget(zd,"zspin","rim","hbr","0|9|0.1|2","space=5");
   zdialog_add_widget(zd,"label","space","hbr",0,"space=6");
   zdialog_add_widget(zd,"label","labc","hbr",Bcolor,"space=3");
   zdialog_add_widget(zd,"colorbutt","color","hbr","0|0|0","space=3");


   width = 40;
   rim = 2;
   color[0] = color[1] = color[2] = 0;

   zdialog_restore_inputs(zd);                                                   //  restore previous inputs
   zdialog_run(zd,escher_spiral_dialog_event,"save");

   takeMouse(escher_spiral_mousefunc,dragcursor);
   return;
}


//  dialog event and completion callback function

int escher_spiral_dialog_event(zdialog *zd, cchar *event)
{
   using namespace escher_spiral_names;
   
   void   escher_spiral_mousefunc();

   char     text[20];
   cchar    *pp;

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()

   if (strmatch(event,"focus"))                                                  //  toggle mouse capture
      takeMouse(escher_spiral_mousefunc,dragcursor);                             //  connect mouse function

   if (zd->zstat)
   {  
      if (zd->zstat == 1)                                                        //  apply
      {
         zd->zstat = 0;
         signal_thread();
         return 1;
      }

      if (zd->zstat == 2) edit_done(0);                                          //  done
      else edit_cancel(0);                                                       //  discard edit
   }
   
   if (zstrstr("width rim color",event))
   {
      zdialog_fetch(zd,"width",width);
      zdialog_fetch(zd,"rim",rim);

      zdialog_fetch(zd,"color",text,20);
      pp = strField(text,"|",1);
      if (pp) color[0] = atoi(pp);
      pp = strField(text,"|",2);
      if (pp) color[1] = atoi(pp);
      pp = strField(text,"|",3);
      if (pp) color[2] = atoi(pp);
   }

   return 1;
}


//  mouse function

void escher_spiral_mousefunc()
{
   using namespace escher_spiral_names;

   if (! LMclick) return;                                                        //  wait for left click
   cx = Mxposn;                                                                  //  mouse position = new center
   cy = Myposn;
   LMclick = Mxdrag = Mydrag = 0;

   signal_thread();
   return;
}


//  main thread function

void * escher_spiral_thread(void *)
{
   using namespace escher_spiral_names;

   void  escher_spiral_function();
   
   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request
      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint

      escher_spiral_function();                                                  //  transform image

      paintlock(0);                                                              //  unblock window paint

      CEF->Fmods++;                                                              //  E3 modified, not saved
      CEF->Fsaved = 0;
      Fpaint2();                                                                 //  update output image
   }

   return 0;                                                                     //  not executed, stop g++ warning
}


void escher_spiral_function()
{
   using namespace escher_spiral_names;
   
   float    revs, rad, step;
   float    sr, sr1, sr2, dr, dr1, dr2;
   float    T, W, F;
   float    dx, dy, sx, sy;
   float    *dpix, spix[4];
   int      px, py, cc, ii, *Npix;
   int      pcc = 3 * sizeof(float);
   
   cc = Eww * Ehh * pcc;                                                         //  clear dest. image
   memset(E3pxm->pixels,0,cc);      

   cc = Eww * Ehh * sizeof(int);                                                 //  clear dest. image pixel counts
   Npix = (int *) zmalloc(cc);
   memset(Npix,0,cc);
   
   revs = 0;                                                                     //  source image sweeps (revolutions)
   
   rad = cx;                                                                     //  radius is nearest edge
   if (Eww - cx < rad) rad = Eww - cx;                                           //    from designated center
   if (cy < rad) rad = cy;
   if (Ehh - cy < rad) rad = Ehh - cy; 

   W = 0.01 * (100 - width);                                                     //  width = 25%  >>  W = 0.75

   sr2 = rad;                                                                    //  source image segment outer radius
   sr1 = sr2 * W;                                                                //  inner radius
   
   dr2 = rad;                                                                    //  initial dest. segment outer radius
   dr1 = dr2 * W;                                                                //  inner radius

   revs = 0;                                                                     //  sections revolutions

   while (true)
   {
      step = 0.7 / dr2;                                                          //  0.7 pixels at radius dr2
      revs += step / (2 * PI);                                                   //  rotate segments by tiny step
      
      F = pow(W,revs);                                                           //  1.0  0.75  0.56  0.42  0.32 ...
      
      dr2 = rad * F;                                                             //  dest. image segment outer radius
      dr1 = dr2 * W;                                                             //  inner radius

      if (dr1 < 2) break;
      
      T = 2 * PI * revs - 0.5 * PI;                                              //  segment angle 

      for (sr = sr1; sr <= sr2; sr++)                                            //  loop source image segment position
      {
         dr = dr1 + (dr2 - dr1) * (sr - sr1) / (sr2 - sr1);                      //  corresp. dest. image segment position
         
         if (100 * (sr2 - sr) / (sr2 - sr1) <= rim)                              //  source pixel > rim threshold
            memcpy(spix,color,pcc); 
         
         else {
            sx = sr * cos(T) + cx;                                               //  source image virtual pixel
            sy = sr * sin(T) + cy;
            vpixel(E1pxm,sx,sy,spix);
         }

         dx = dr * cos(T) + cx;                                                  //  dest. image pixel
         dy = dr * sin(T) + cy;
         
         px = int(dx);
         py = int(dy);
         dpix = PXMpix(E3pxm,px,py);
         
         dpix[0] += spix[0];                                                     //  add source to dest. pixel
         dpix[1] += spix[1];
         dpix[2] += spix[2];
         
         ii = py * Eww + px;                                                     //  count pixels added.
         ++Npix[ii];
      }
   }

   for (py = 0; py < Ehh; py++)                                                  //  loop dest. pixels
   for (px = 0; px < Eww; px++)   
   {
      ii = py * Eww + px;                                                        //  get count of accumulated dest. pixels
      ii = Npix[ii];

      if (ii > 1) {                                                              //  > 1: make average pixel
         dpix = PXMpix(E3pxm,px,py);
         dpix[0] = dpix[0] / ii;
         dpix[1] = dpix[1] / ii;
         dpix[2] = dpix[2] / ii;
      }

      else if (ii == 0)                                                          //  0: copy source pixel
         memcpy(PXMpix(E3pxm,px,py),PXMpix(E1pxm,px,py),pcc);
   }
   
   zfree(Npix);
   return;
}


