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

   Fotocx - edit photos and manage collections

   Copyright 2007-2024 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.

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

   Fotocx image edit - warp menu functions

   m_unbend                straighten curved image from 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_warp_radial           add or remove barrel or pincushion distortion
   m_unwarp_closeup        close-up face photo, remove "baloon face" distortioin
   m_makewaves             distort an image with a wave pattern
   m_twist                 twist image centered at mouse position
   m_sphere                sphere projection, image shrinks 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
   m_flatphoto             flatten photo of a curved surface

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

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

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


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

namespace unbend_names
{
   float    ub_lin_horz, ub_lin_vert;                                            //  unbend values from dialog
   float    ub_cur_horz, ub_cur_vert;
   float    ub_x1, ub_x2, ub_y1, ub_y2;                                          //  unbend axes scaled 0 to 1
   int      ub_hx1, ub_hy1, ub_hx2, ub_hy2;
   int      ub_vx1, ub_vy1, ub_vx2, ub_vy2;

   editfunc    EFunbend;
}


//  menu function

void m_unbend(GtkWidget *, ch *menu)
{
   using namespace unbend_names;

   int    unbend_dialog_event(zdialog* zd, ch *event);
   void * unbend_thread(void *);
   void   unbend_mousefunc();

   F1_help_topic = "unbend";

   Plog(1,"m_unbend \n");

   EFunbend.menuname = "Unbend";
   EFunbend.FprevReq = 1;                                                        //  use preview
   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         |
         |   _____               |
         |  |     |   [____]     |
         |  |_____|              |
         |   _____               |
         |  |     |   [____]     |
         |  |_____|              |
         |   _____               |
         |  |     |   [____]     |
         |  |_____|              |
         |   _____               |
         |  |     |   [____]     |
         |  |_____|              |
         |                       |
         |              [OK] [X] |
         |_______________________|

***/

   zdialog *zd = zdialog_new("Unbend",Mwin,"OK"," X ",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","-300|300|1|0");              //  finer steps
   zdialog_add_widget(zd,"zspin","spcurvert","vb2","-300|300|1|0");
   zdialog_add_widget(zd,"zspin","splinhorz","vb2","-300|300|1|0");
   zdialog_add_widget(zd,"zspin","spcurhorz","vb2","-300|300|1|0");

   ub_x1 = ub_x2 = ub_y1 = ub_y2 = 0.5;                                          //  initial axes thru image middle
   ub_lin_horz = ub_lin_vert = 0;
   ub_cur_horz = ub_cur_vert = 0;

   takeMouse(unbend_mousefunc,dragcursor);                                       //  connect mouse function
   thread_signal();
   zdialog_run(zd,unbend_dialog_event,"save");                                   //  run dialog, parallel
   return;
}


//  dialog event and completion callback function

int unbend_dialog_event(zdialog *zd, ch *event)
{
   using namespace unbend_names;

   void unbend_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 (zd->zstat)                                                                //  dialog complete
   {
      if (zd->zstat == 1) {                                                      //  done
         CEF->Fmods = 0;
         if (ub_cur_vert || ub_cur_horz ||                                       //  image3 modified
             ub_lin_vert || ub_lin_horz) {
            CEF->Fmods = 1;
            CEF->Fsaved = 0;                                                     //  done
         }
         edit_fullsize();                                                        //  get full size image
         thread_signal();
         edit_addhist("%.0f %.0f %.0f %.0f",ub_lin_vert,ub_cur_vert,             //  edit parms > edit hist
                                            ub_lin_horz, ub_cur_horz);
         draw_toplines(2,0);                                                     //  erase axes-lines
         edit_done(0);                                                           //  commit edit
      }

      else edit_cancel(0);                                                       //  discard edit
      draw_toplines(2,0);                                                        //  erase axes-lines
      return 1;
   }

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

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

   if (strmatch(event,"spcurvert")) {
      zdialog_fetch(zd,"spcurvert",ub_cur_vert);
      thread_signal();
   }

   if (strmatch(event,"splinhorz")) {
      zdialog_fetch(zd,"splinhorz",ub_lin_horz);
      thread_signal();
   }

   if (strmatch(event,"spcurhorz")) {
      zdialog_fetch(zd,"spcurhorz",ub_cur_horz);
      thread_signal();
   }

   return 1;
}


//  unbend mouse function                                                        //  adjustable axes

void unbend_mousefunc()
{
   using namespace unbend_names;

   ch          *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 / Eww;                                                        //  scale mouse position 0 to 1
   mpy = 1.0 * mpy / Ehh;

   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-ub_y1) * (mpy-ub_y1);
   if (dist2 < dist1) {
      dist1 = dist2;
      close = "left";
   }

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

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

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

   if (strmatch(close,"left")) ub_y1 = mpy;                                      //  set new axis end-point
   if (strmatch(close,"right")) ub_y2 = mpy;
   if (strmatch(close,"top")) ub_x1 = mpx;
   if (strmatch(close,"bottom")) ub_x2 = mpx;

   thread_signal();                                                              //  trigger thread

   LMclick = Mxdrag = Mydrag = 0;
   return;
}


//  unbend thread function

void * unbend_thread(void *arg)
{
   using namespace unbend_names;

   void * unbend_wthread(void *);

   ub_hx1 = 0;                                                                   //  scale axes to Eww/hh
   ub_hy1 = ub_y1 * Ehh;
   ub_hx2 = Eww;
   ub_hy2 = ub_y2 * Ehh;

   ub_vx1 = ub_x1 * Eww;
   ub_vy1 = 0;
   ub_vx2 = ub_x2 * Eww;
   ub_vy2 = Ehh;

   Ntoplines = 2;
   toplines[0].x1 = ub_hx1;                                                      //  lines on window
   toplines[0].y1 = ub_hy1;
   toplines[0].x2 = ub_hx2;
   toplines[0].y2 = ub_hy2;
   toplines[0].type = 3;                                                         //  black/white pair
   toplines[1].x1 = ub_vx1;
   toplines[1].y1 = ub_vy1;
   toplines[1].x2 = ub_vx2;
   toplines[1].y2 = ub_vy2;
   toplines[1].type = 3;

   do_wthreads(unbend_wthread,NSMP);                                             //  worker threads

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

   Fpaint2();                                                                    //  update window
   return 0;
}


void * unbend_wthread(void *arg)                                                 //  worker thread function
{
   using namespace unbend_names;

   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(ub_cur_vert * 0.0033 * Ehh);                                    //  -0.99 to +0.99
   curhorz = int(ub_cur_horz * 0.0033 * Eww);
   linvert = int(ub_lin_vert * 0.00043 * Ehh);                                   //  -0.13 to +0.13
   linhorz = int(ub_lin_horz * 0.00043 * Eww);

   vx1 = ub_vx1;
   vx2 = ub_vx2;
   hy1 = ub_hy1;
   hy2 = ub_hy2;

   for (py3 = index; py3 < Ehh; py3 += NSMP)                                     //  step through F3 pixels
   for (px3 = 0; px3 < Eww; px3++)
   {
      cx3 = vx1 + (vx2 - vx1) * py3 / Ehh;                                       //  center of unbend
      cy3 = hy1 + (hy2 - hy1) * px3 / Eww;
      dispx = 2.0 * (px3 - cx3) / Eww;                                           //  -1.0 ..  0.0 .. +1.0  (roughly)
      dispy = 2.0 * (py3 - cy3) / Ehh;                                           //  -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);
   }

   return 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
   ch          PSP_pixlab[4][4] = { " A ", " B ", " C ", " D " };
   int         PSP_corner = 0;

   editfunc    EFperspective;
}


//  menu function

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

   int   perspective_dialog_event(zdialog *zd, ch *event);
   void  perspective_mousefunc(void);

   ch     *PSP_message =
          " 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";

   Plog(1,"m_perspective \n");

   EFperspective.menufunc = m_perspective;
   EFperspective.menuname = "Perspective";
   EFperspective.mousefunc = perspective_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("Fix Perspective",Mwin,"Apply","Reset","Crop","OK",null);  
   zdialog_add_widget(zd,"label","lab1","dialog",PSP_message,"space=3");

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

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


//  dialog completion callback function

int perspective_dialog_event(zdialog *zd, ch *event)
{
   using namespace perspective;

   void  perspective_warpfunc(void);
   void perspective_mousefunc();

   int      ii, px, py;

   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
      takeMouse(perspective_mousefunc,dragcursor);

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

   if (zd->zstat == 1)                                                           //  apply
   {
      erase_toptext(102);                                                        //  erase points
      perspective_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) {                                                    //  crop                                  24.20
      erase_toptext(102);
      edit_done(0);
      m_crop(0,0);
   }

   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 * Eww
          && abs(PSP_pixel[ii][1] - Myclick) < 0.07 * Ehh)
         {
            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;
}


//  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,"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];                                                       //  displacement 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);

   Funcbusy(+1);                                                                 //  24.10

   for (py3 = 0; py3 < Ehh; py3++)                                               //  loop all output pixels
   for (px3 = 0; px3 < Eww; 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
   }

   Funcbusy(-1);

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

   Fpaint2();                                                                    //  update window
   return;
}


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

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

namespace warp_area
{
   float       *dragx, *dragy;                                                   //  memory for pixel warp vectors
   int         wacc, Fexpand;
   int         Fstarted;
   int         area_num;

   editfunc    EFwarparea;
}


//  menu function

void m_warp_area(GtkWidget *, ch *menu)
{
   using namespace warp_area;

   int   warp_area_dialog_event(zdialog *zd, ch *event);
   void  warp_area_mousefunc();

   ch     *warp_area_message =
             " 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 [OK].";

   F1_help_topic = "warp area";

   Plog(1,"m_warp_area \n");

   EFwarparea.menufunc = m_warp_area;
   EFwarparea.menuname = "Warp Area";
   EFwarparea.Farea = 2;                                                         //  select area usable
   EFwarparea.mousefunc = warp_area_mousefunc;                                   //  mouse function
   if (! edit_setup(EFwarparea)) return;                                         //  setup edit

   zdialog *zd = zdialog_new("Warp Area",Mwin,"OK"," X ",null);
   EFwarparea.zd = zd;

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

   wacc = Eww * Ehh;
   dragx = (float *) zmalloc(wacc * sizeof(float),"warp area");                  //  get memory for pixel warp vectors
   dragy = (float *) zmalloc(wacc * sizeof(float),"warp area");

   Fstarted = 0;
   area_num = 0;
   Fexpand = 0;

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


//  dialog event and completion callback function

int warp_area_dialog_event(zdialog * zd, ch *event)
{
   using namespace warp_area;

   void  warp_area_mousefunc();

   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(dragx);                                                              //  release memory
      zfree(dragy);
      return 1;
   }

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

   if (sa_stat != sa_stat_fini)                                                  //  no select area active
      init = 1;

   if (Fstarted && area_num != areanumber)                                       //  select area changed
      init = 1;

   if (init) {
      memset(dragx,0,wacc);
      memset(dragy,0,wacc);
      Fstarted = 0;
      area_num = 0;
      return 1;
   }

   if (strmatch(event,"start"))                                                  //  start warp
   {
      if (sa_stat != sa_stat_fini) {                                             //  no select area active
         zmessageACK(Mwin,"no active Select Area");
         return 1;
      }
      sa_edgecalc();                                                             //  calculate area edge distances
      takeMouse(warp_area_mousefunc,dragcursor);                                 //  connect mouse function
      Fstarted = 1;
      area_num = areanumber;
   }

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

   if (strmatch(event,"reset")) {                                                //  undo all warps
      edit_reset();
      memset(dragx,0,wacc);                                                      //  clear warp vectors
      memset(dragy,0,wacc);
   }

   return 1;
}


//  warp mouse function

void warp_area_mousefunc()
{
   using namespace warp_area;

   void  warp_area_warpfunc(float mdx, float mdy, float mdw, float mdh, int acc);
   void  warp_area_expand();

   static float   mdx, mdy, mdw, mdh;
   static int     warped = 0;

   if (! Fstarted) 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;
      warp_area_warpfunc(mdx,mdy,mdw,mdh,0);                                     //  warp image
      warped = 1;
      Mxdrag = Mydrag = 0;
      return;
   }

   else if (warped)
   {
      warped = 0;
      warp_area_warpfunc(mdx,mdy,mdw,mdh,1);                                     //  drag done, update warp vectors
      if (Fexpand) warp_area_expand();
   }

   return;
}


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

void warp_area_warpfunc(float mdx, float mdy, float mdw, float mdh, int acc)
{
   using namespace warp_area;

   void  warp_area_warpfunc2(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 != sa_stat_fini) return;                                          //  area erased

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

   ii = mdy * ww + mdx;
   if (! sa_pixmap[ii]) {                                                        //  if mouse drag outside select area
      warp_area_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 += dragx[ii];                                                        //  add this warp to prior
      dispy += dragy[ii];

      if (acc) {                                                                 //  mouse drag done,
         dragx[ii] = dispx;                                                      //    accumulate warp memory
         dragy[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 warp_area_warpfunc2(float mdx, float mdy, float mdw, float mdh, int acc)
{
   using namespace warp_area;

   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))

   Fexpand = 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 += dragx[ii];                                                        //  add this warp to prior
      dispy += dragy[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,
         dragx[ii] = dispx;                                                      //    accumulate warp memory
         dragy[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 warp_area_expand()
{
   using namespace warp_area;

   int      ii, px, py, ww, hh;

   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 (! dragx[ii] && ! dragy[ii]) continue;
      sa_pixmap[ii] = 1;                                                         //  mark within area
   }

   sa_map_pixels();
   sa_finish_auto();

   area_num = areanumber;
   Fexpand = 0;

   return;
}


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

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

namespace warp_curved_names
{
   float       *dragx, *dragy;                                                   //  memory of all dragged pixels
   float       dragmem[5][100];                                                  //  undo memory, last 100 drags
   int         Ndrag;                                                            //  dragmem count
   int         Fdrag;
   int         Pww, Phh;

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

   editfunc    EFwarp_curved;
}


//  menu function

void m_warp_curved(GtkWidget *, ch *menu)
{
   using namespace warp_curved_names;

   int    warp_curved_dialog_event(zdialog *zd, ch *event);
   void   warp_curved_mousefunc(void);

   ch     *warp_curved_message =
             " Pull an image position using the mouse. \n"
             " Make multiple mouse pulls until satisfied. \n"
             " When finished, press [OK].";

   int         px, py, ii;
   int64       cc64;

   F1_help_topic = "warp curved";

   Plog(1,"m_warp_curved \n");

   EFwarp_curved.menufunc = m_warp_curved;
   EFwarp_curved.menuname = "Warp Curved";
   EFwarp_curved.FprevReq = 1;                                                   //  use preview
   EFwarp_curved.mousefunc = warp_curved_mousefunc;                                          //  mouse function
   if (! edit_setup(EFwarp_curved)) return;                                      //  setup edit

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

   zdialog *zd = zdialog_new("Warp Curved",Mwin,"OK"," X ",null);
   EFwarp_curved.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",warp_curved_message,"space=3");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=8");
   zdialog_add_widget(zd,"button","undolast","hb1","Undo Last","space=8");
   zdialog_add_widget(zd,"button","undoall","hb1","Undo All","space=2");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=4");
   zdialog_add_widget(zd,"label","lab2","hb2","warp span","space=8");
   zdialog_add_widget(zd,"zspin","span","hb2","0.01|2.0|0.01|0.1","space=1");

   Ndrag = Fdrag = 0;                                                            //  no drag data

   cc64 = imagesize(Eww, Ehh, sizeof(float),1);
   dragx = (float *) zmalloc(cc64,"warp curved");                                //  get memory for pixel displacements
   dragy = (float *) zmalloc(cc64,"warp curved");

   for (py = 0; py < Ehh; py++)                                                  //  no pixel displacements
   for (px = 0; px < Eww; px++)
   {
      ii = py * Eww + px;
      dragx[ii] = dragy[ii] = 0.0;
   }

   Pww = Eww;                                                                    //  save preview dimensions
   Phh = Ehh;

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

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


//  dialog event and completion callback function

int warp_curved_dialog_event(zdialog * zd, ch *event)
{
   using namespace warp_curved_names;

   void  warp_curved_warpfunc();
   void warp_curved_mousefunc();

   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,"focus"))                                                  //  toggle mouse capture                  24.10
      takeMouse(warp_curved_mousefunc,dragcursor);

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

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

      for (py = 0; py < Ehh; py++)                                               //  reset pixel displacements
      for (px = 0; px < Eww; px++)
      {
         ii = py * Eww + px;
         dragx[ii] = dragy[ii] = 0.0;
      }
      edit_reset();                                                              //  restore image 1
   }

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

   return 1;

complete:

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

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

         scale = 1.0 * (Eww + Ehh) / (Pww + Phh);                                //  full size / preview size

         for (fpy = 0; fpy < Ehh; fpy++)                                         //  scale net pixel displacements
         for (fpx = 0; fpx < Eww; fpx++)                                         //    to full image size
         {
            epx = Pww * fpx / Eww;
            epy = Phh * fpy / Ehh;
            ii = epy * Pww + epx;
            dispx = dragx[ii] * scale;
            dispy = dragy[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
         }

         Fpaint2();

         edit_done(0);                                                           //  commit edit
      }

      else edit_cancel(0);                                                       //  discard edit

      zfree(dragx);                                                              //  release memory
      zfree(dragy);
   }

   return 1;
}


//  warp_curved mouse function

void warp_curved_mousefunc()
{
   using namespace warp_curved_names;

   void   warp_curved_warpfunc();

   int      ii;

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

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

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

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

   return;
}


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

void warp_curved_warpfunc()
{
   using namespace warp_curved_names;

   void * warp_curved_wthread(void *arg);

   float       D, d1, d2, d3, d4;

   d1 = ($mdx-0) * ($mdx-0) + ($mdy-0) * ($mdy-0);                               //  distance, mouse to 4 corners
   d2 = (Eww-$mdx) * (Eww-$mdx) + ($mdy-0) * ($mdy-0);
   d3 = (Eww-$mdx) * (Eww-$mdx) + (Ehh-$mdy) * (Ehh-$mdy);
   d4 = ($mdx-0) * ($mdx-0) + (Ehh-$mdy) * (Ehh-$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 * $span;

   do_wthreads(warp_curved_wthread,NSMP);                                        //  worker threads

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


//  working thread to process the pixels

void * warp_curved_wthread(void *arg)
{
   using namespace warp_curved_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 < Ehh; py += NSMP)                                        //  process all pixels
   for (px = 0; px < Eww; 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 * Eww + px;

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

      dispx += dragx[ii];                                                        //  add this drag to prior sum
      dispy += dragy[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
   }

   return 0;                                                                     //  exit thread
}


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

//  warp/distort whole image with a linear transform

namespace warp_linear_names
{
   float       *dragx, *dragy;                                                   //  memory of all dragged pixels
   float       dragmem[5][100];                                                  //  undo memory, last 100 drags
   int         Ndrag;                                                            //  dragmem count
   int         Pww, Phh;                                                         //  preview image dimensions

   float       mx1, my1, mx2, my2;                                               //  mouse drag start, stop
   float       span, maxspan, fade;
   int         Facc;

   editfunc    EFwarp_linear;
}


//  menu function

void m_warp_linear(GtkWidget *, ch *menu)                                        //  overhaul      24.10
{
   using namespace warp_linear_names;

   int    warp_linear_dialog_event(zdialog *zd, ch *event);
   void   warp_linear_mousefunc(void);

   ch     *warp_linear_message =
             " Pull an image position using the mouse. \n"
             " Make multiple mouse pulls until satisfied. \n"
             " When finished, press [OK].";

   int         px, py, ii;
   int64       cc64;

   F1_help_topic = "warp linear";

   Plog(1,"m_warp_linear \n");

   EFwarp_linear.menufunc = m_warp_linear;
   EFwarp_linear.menuname = "Warp Linear";
   EFwarp_linear.FprevReq = 1;                                                   //  use preview
   EFwarp_linear.mousefunc = warp_linear_mousefunc;                              //  mouse function
   if (! edit_setup(EFwarp_linear)) return;                                      //  setup edit

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

/**
       _________________________________________
      |              Warp Linear                |
      |                                         |
      | Pull an image position using the mouse. |
      | Make multiple mouse pulls as needed.    |
      | When finished, press [OK].              |
      |                                         |
      | [Undo Last]  [Undo All]                 |
      | warp span [ 0.5 ]  fade [ 0.5 ]         |
      |                                         |
      |                                [OK] [X] |
      |_________________________________________|

**/


   zdialog *zd = zdialog_new("Warp Linear",Mwin,"OK"," X ",null);
   EFwarp_linear.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",warp_linear_message,"space=3");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=8");
   zdialog_add_widget(zd,"button","undolast","hb1","Undo Last","space=3");
   zdialog_add_widget(zd,"button","undoall","hb1","Undo All","space=8");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labspan","hb2","warp span","space=3");
   zdialog_add_widget(zd,"zspin","span","hb2","0.01|2.0|0.01|1.0","space=3");
   zdialog_add_widget(zd,"label","space","hb2",0,"space=10");
   zdialog_add_widget(zd,"label","labfade","hb2","warp fade","space=3");
   zdialog_add_widget(zd,"zspin","fade","hb2","0.0|1.0|0.01|1.0","space=3");

   maxspan = sqrtf(Eww*Eww + Ehh*Ehh);                                           //  max. distance in image

   Ndrag = 0;                                                                    //  no drag data

   cc64 = imagesize(Eww, Ehh, sizeof(float),1);
   dragx = (float *) zmalloc(cc64,"warp linear");                                //  get memory for pixel displacements
   dragy = (float *) zmalloc(cc64,"warp linear");

   for (py = 0; py < Ehh; py++)                                                  //  no pixel displacements
   for (px = 0; px < Eww; px++)
   {
      ii = py * Eww + px;
      dragx[ii] = dragy[ii] = 0.0;
   }

   Pww = Eww;                                                                    //  save preview dimensions
   Phh = Ehh;

   zdialog_fetch(zd,"span",span);                                                //  initial values
   zdialog_fetch(zd,"fade",fade);

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


//  dialog event and completion callback function

int warp_linear_dialog_event(zdialog * zd, ch *event)
{
   using namespace warp_linear_names;

   void  warp_linear_warpfunc();
   void warp_linear_mousefunc();

   int         px, py, ii;
   int         fpx, fpy, epx, epy, vstat;
   float       scale, dispx, dispy, span2;
   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,"focus"))                                                  //  toggle mouse capture                  24.10
      takeMouse(warp_linear_mousefunc,dragcursor);

   if (strmatch(event,"undolast"))
   {
      if (Ndrag == 1) event = "undoall";
      else if (Ndrag) {                                                          //  undo most recent drag
         ii = --Ndrag;
         mx1 = dragmem[0][ii];
         my1 = dragmem[1][ii];
         mx2 = -dragmem[2][ii];
         my2 = -dragmem[3][ii];
         span2 = dragmem[4][ii];
         zdialog_stuff(zd,"span",span2);
         Facc = 0;
         warp_linear_warpfunc();                                                 //  undrag image
         Facc = 1;
         warp_linear_warpfunc();                                                 //  undrag memory
      }
   }

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

      for (py = 0; py < Ehh; py++)                                               //  reset pixel displacements
      for (px = 0; px < Eww; px++)
      {
         ii = py * Eww + px;
         dragx[ii] = dragy[ii] = 0.0;
      }
      edit_reset();                                                              //  restore image 1
   }

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

   if (strmatch(event,"fade"))
      zdialog_fetch(zd,"fade",fade);

   return 1;

complete:

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

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

         scale = 1.0 * (Eww + Ehh) / (Pww + Phh);                                //  full size / preview size

         for (fpy = 0; fpy < Ehh; fpy++)                                         //  scale net pixel displacements
         for (fpx = 0; fpx < Eww; fpx++)                                         //    to full image size
         {
            epx = Pww * fpx / Eww;
            epy = Phh * fpy / Ehh;
            ii = epy * Pww + epx;
            dispx = dragx[ii] * scale;
            dispy = dragy[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
         }

         Fpaint2();

         edit_done(0);                                                           //  commit edit
      }

      else edit_cancel(0);                                                       //  discard edit

      zfree(dragx);                                                              //  release memory
      zfree(dragy);
   }

   return 1;
}


//  warp_linear mouse function

void warp_linear_mousefunc()
{
   using namespace warp_linear_names;

   void   warp_linear_warpfunc();

   int      ii;

   if (Mxdrag || Mydrag)                                                         //  mouse drag underway
   {
      mx1 = Mxdown;                                                              //  drag origin
      my1 = Mydown;
      mx2 = Mxdrag - Mxdown;                                                     //  drag increment
      my2 = Mydrag - Mydown;
      Facc = 0;
      warp_linear_warpfunc();                                                    //  drag image
      Mxdrag = Mydrag = 0;
      return;
   }

   else if (Facc == 0)
   {
      Facc = 1;
      warp_linear_warpfunc();                                                    //  drag done, add to memory

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

      ii = Ndrag;
      dragmem[0][ii] = mx1;                                                      //  save drag for undo
      dragmem[1][ii] = my1;
      dragmem[2][ii] = mx2;
      dragmem[3][ii] = my2;
      dragmem[4][ii] = span;
      Ndrag++;
   }

   return;
}


//  warp image and accumulate warp memory
//  mouse at (mx1,my1) is moved (mx2,my2) pixels (+/-)

void warp_linear_warpfunc()
{
   using namespace warp_linear_names;

   void * warp_linear_wthread(void *arg);

   do_wthreads(warp_linear_wthread,NSMP);                                        //  worker threads

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


//  working thread to process the pixels
//  compute distance of each pixel from line perpendicular to mouse drag and intersecting drag origin

void * warp_linear_wthread(void *arg)
{
   using namespace warp_linear_names;

   float point_line_dist(float lx, float ly, float lm, float px, float py);

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

   dragspan = maxspan * span;                                                    //  image span affected by mouse drags

   dx = mx2;                                                                     //  mouse drag vector from start position
   dy = my2;

   for (py = index; py < Ehh; py += NSMP)                                        //  process all pixels px/py
   for (px = 0; px < Eww; px++)
   {
      if (! dy) pdist = fabsf(px - mx1);                                         //  px/py distance from line perpendicular
      else {                                                                     //    through mouse drag start at mx1/my1
         m = - dx / dy;                                                          //  slope of line perpendicular to mouse drag
         pdist = point_line_dist(mx1,my1,m,px,py);                               //  px/py distance to above line
      }

      mag = (dragspan - pdist) / dragspan;                                       //  scale pixel movement  1 .. 0
      if (mag <= 0) continue;
      
      mag = mag * (1.0 - fade) + mag * mag * fade;                               //  attenuated movement 

      dispx = -dx * mag;                                                         //  displacement = drag * mag
      dispy = -dy * mag;

      ii = py * Eww + px;

      if (Facc) {                                                                //  drag done, accumulate drag sum
         dragx[ii] += dispx;
         dragy[ii] += dispy;
         continue;
      }

      dispx += dragx[ii];                                                        //  add this drag to prior sum
      dispy += dragy[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
   }

   return 0;                                                                     //  exit thread
}


//  compute minimum distance of a point to a line
//  lx, ly: a point in the line
//  lm: the slope of the line
//  px, py: a point to calculate distance to line
//  if lm is NAN, the line is vertical and the distance is |px-lx|

float point_line_dist(float lx, float ly, float lm, float px, float py)
{
   float    a, b, c, dist;

   if (isnan(lm)) return fabsf(px-lx);

   a = lm;                             //  get line equation
   b = -1;                             //  a*x + b*y + c = 0
   c = ly - lm * lx;

   dist = fabsf(a*px + b*py + c) / sqrtf(a*a + b*b);

   return dist;
}


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

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

namespace warp_affine
{
   float       waf_old[3][2];                                                    //  3 original image points
   float       waf_new[3][2];                                                    //  corresponding warped points
   float       waf_coeff[6];                                                     //  transform coefficients
   float       waf_Icoeff[6];                                                    //  inverse transform coefficients
   int         waf_ftf;                                                          //  first time flag

   editfunc    EFwarpareaF;
}


//  menu function

void m_warp_affine(GtkWidget *, ch *menu)
{
   using namespace warp_affine;

   int   warp_affine_dialog_event(zdialog *zd, ch *event);
   void  warp_affine_mousefunc(void);

   ch     *warp_affine_message =
             " Pull on an image corner using the mouse. \n"
             " Make multiple mouse pulls until satisfied. \n"
             " When finished, press [OK].";

   F1_help_topic = "warp affine";

   Plog(1,"m_warp_affine \n");

   EFwarpareaF.menufunc = m_warp_affine;
   EFwarpareaF.menuname = "Warp Affine";
   EFwarpareaF.FprevReq = 1;                                                     //  use preview
   EFwarpareaF.mousefunc = warp_affine_mousefunc;                                //  mouse function
   if (! edit_setup(EFwarpareaF)) return;                                        //  setup edit

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

   zdialog *zd = zdialog_new("Warp Affine",Mwin,"OK"," X ",null);
   EFwarpareaF.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",warp_affine_message,"space=3");

   waf_ftf = 1;                                                                  //  1st warp flag

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

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


//  dialog event and completion callback function

int warp_affine_dialog_event(zdialog *zd, ch *event)
{
   using namespace warp_affine;

   void  warp_affine_warpfunc();                                                 //  image warp function
   void warp_affine_mousefunc();

   float       scale;
   int         Pww, Phh;

   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                  24.10
      takeMouse(warp_affine_mousefunc,dragcursor);

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

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

   Pww = Eww;                                                                    //  preview image dimensions
   Phh = Ehh;

   edit_fullsize();                                                              //  get full-size images

   scale = 1.0 * (Eww + Ehh) / (Pww + Phh);                                      //  preview to full-size scale factor

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

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

   warp_affine_warpfunc();                                                       //  warp full-size image

   edit_done(0);
   return 1;
}


//  warpAF mouse function

void warp_affine_mousefunc()
{
   using namespace warp_affine;

   void  warp_affine_warpfunc();                                                 //  image warp function
   void  warp_affine_invert(float coeff[6], float Icoeff[6]);                    //  compute reverse transform coefficients

   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 = Eww - x1n;                                                              //  point 2 = mirror of point1
   y2n = Ehh - y1n;
   x3n = Eww * (y2n / Ehh);
   y3n = Ehh * (1.0 - (x2n / Eww));

   if (waf_ftf)                                                                  //  first warp
   {
      waf_ftf = 0;
      x1o = x1n;                                                                 //  old = current positions
      y1o = y1n;
      x2o = x2n;
      y2o = y2n;
      x3o = x3n;
      y3o = y3n;
   }
   else
   {
      warp_affine_invert(waf_coeff,waf_Icoeff);                                  //  get inverse coefficients
      a = waf_Icoeff[0];
      b = waf_Icoeff[1];
      c = waf_Icoeff[2];
      d = waf_Icoeff[3];
      e = waf_Icoeff[4];
      f = waf_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;
   }

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

   x1n = mdx2;                                                                   //  point 1 new position = drag position
   y1n = mdy2;
   x2n = Eww - x1n;                                                              //  point 2 new = mirror of point1 new
   y2n = Ehh - y1n;

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

   warp_affine_warpfunc();                                                       //  do the warp

   Mxdrag = Mydrag = 0;
   return;
}


//  warp image and accumulate warp memory

void  warp_affine_warpfunc()
{
   using namespace warp_affine;

   void   warp_affine_affine(float po[3][2], float pn[3][2], float coeff[6]);    //  compute affine transform coefficients
   void   warp_affine_invert(float coeff[6], float Icoeff[6]);                   //  compute reverse transform coefficients
   void * warp_affine_wthread(void *);

   warp_affine_affine(waf_old, waf_new, waf_coeff);                              //  get coefficients for forward transform
   warp_affine_invert(waf_coeff, waf_Icoeff);                                    //  get coefficients for reverse transform

   do_wthreads(warp_affine_wthread,NSMP);                                        //  worker threads

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


//  working thread to process the pixels

void * warp_affine_wthread(void *arg)
{
   using namespace warp_affine;

   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 = waf_Icoeff[0];                                                            //  coefficients to map output pixels
   b = waf_Icoeff[1];                                                            //    to corresponding input pixels
   c = waf_Icoeff[2];
   d = waf_Icoeff[3];
   e = waf_Icoeff[4];
   f = waf_Icoeff[5];

   for (py3 = index; py3 < Ehh; py3 += NSMP)                                     //  process all pixels
   for (px3 = 0; px3 < Eww; 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
   }

   return 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 warp_affine_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 warp_affine_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;
}


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

//  Add or remove barrel or pincushion distortion.

namespace warp_radial_names
{
   int         Pww, Phh;                                                         //  preview dimensions
   int         Xcen, Ycen;                                                       //  image center (mouse select)
   float       R;                                                                //  max. center - edge distance
   float       stretch;                                                          //  stretch parameter
   editfunc    EFwarp_radial;                                                    //  edit function data
}


//  menu function

void m_warp_radial(GtkWidget *, ch *menu)                                        //  rename "stretch"                      24.10
{
   using namespace warp_radial_names;

   int    warp_radial_dialog_event(zdialog* zd, ch *event);
   void   warp_radial_mousefunc(void);
   void * warp_radial_thread(void *);

   ch     *title = "Add/remove barrel/pincushion distortion";

   F1_help_topic = "warp radial";

   Plog(1,"m_warp_radial \n");

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

   EFwarp_radial.menufunc = m_warp_radial;
   EFwarp_radial.menuname = "Warp Radial";
   EFwarp_radial.FprevReq = 1;                                                   //  use preview edit mode
   EFwarp_radial.mousefunc = warp_radial_mousefunc;                              //  mouse function
   EFwarp_radial.threadfunc = warp_radial_thread;                                //  thread function
   if (! edit_setup(EFwarp_radial)) return;                                      //  setup edit

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

   Pww = Eww;                                                                    //  save preview dimensions
   Phh = Ehh;

   Xcen = Eww / 2;                                                               //  set dialog defaults
   Ycen = Ehh / 2;

/***
          _________________________________________
         | Add/Remove barrel/pincushion distortion |
         |                                         |
         | Drag mouse to set center                |
         |                                         |
         | Stretch ============[]================= |
         |                                         |
         |                                [OK] [X] |
         |_________________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,"OK"," X ",null);                        //  warp_radial dialog
   CEF->zd = zd;

   zdialog_add_widget(zd,"label","labtip","dialog","Drag mouse to set center");
   zdialog_add_widget(zd,"hbox","hbstr","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labstr","hbstr","Stretch","space=5");
   zdialog_add_widget(zd,"hscale","stretch","hbstr","-200|+200|0.5|0","expand");

   R = Xcen;                                                                     //  R = greatest center-edge distance
   if (Ycen > R) R = Ycen;
   if (Eww - Xcen > R) R = Eww - Xcen;
   if (Ehh - Ycen > R) R = Ehh - Ycen;

   stretch = 0;                                                                  //  default

   zdialog_resize(zd,300,0);
   zdialog_run(zd,warp_radial_dialog_event,"save");                              //  run dialog - parallel
   takeMouse(warp_radial_mousefunc,0);                                           //  connect mouse function
   thread_signal();                                                              //  trigger update thread
   return;
}


//  warp_radial dialog event and completion function

int warp_radial_dialog_event(zdialog *zd, ch *event)
{
   using namespace warp_radial_names;

   void warp_radial_mousefunc();

   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 (strmatch(event,"focus"))                                                  //  toggle mouse capture                  24.10
      takeMouse(warp_radial_mousefunc,dragcursor);

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  done
      {
         edit_fullsize();                                                        //  get full size image
         scale = 1.0 * (Eww + Ehh) / (Pww + Phh);                                //  scale up the parameters
         Xcen = scale * Xcen;
         Ycen = scale * Ycen;
         stretch = scale * stretch;
         thread_signal();                                                        //  recalculate image
         edit_done(0);                                                           //  commit edit
      }

      else edit_cancel(0);                                                       //  discard edit

      return 1;
   }

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

   return 1;
}


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

void warp_radial_mousefunc()
{
   using namespace warp_radial_names;

   if (! LMclick && ! Mdrag) return;
   LMclick = Mxdrag = Mydrag = 0;

   if (Mxposn < 0.1 * Eww || Mxposn > 0.9 * Eww) return;                         //  ignore if near image edge
   if (Myposn < 0.1 * Ehh || Myposn > 0.9 * Ehh) return;

   Xcen = Mxposn;                                                                //  new center
   Ycen = Myposn;

   thread_signal();

   return;
}


//  thread function - multiple working threads to update image

void * warp_radial_thread(void *)
{
   using namespace warp_radial_names;

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

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

   R = Xcen;                                                                     //  R = greatest center-edge distance
   if (Ycen > R) R = Ycen;
   if (Eww - Xcen > R) R = Eww - Xcen;
   if (Ehh - Ycen > R) R = Ehh - Ycen;

   do_wthreads(warp_radial_wthread,NSMP);                                        //  worker threads

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

   Fpaint2();                                                                    //  update window
   return 0;
}


void * warp_radial_wthread(void *arg)                                            //  worker thread function
{
   using namespace warp_radial_names;

   int         index = *((int *) (arg));
   int         px3, py3, dx, dy, vstat;
   float       s, d, px1, py1, *pix3, vpix[4];
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   for (py3 = index; py3 < Ehh; py3 += NSMP)                                     //  loop output pixels
   for (px3 = 0; px3 < Eww; px3++)
   {
      dx = px3 - Xcen;                                                           //  distance from mouse center
      dy = py3 - Ycen;
      d = sqrtf(dx*dx + dy*dy);                                                  //  center to output pixel

      s = d - stretch * sinf(PI * d/R);                                          //  center to input pixel

      px1 = Xcen + dx * s/d;                                                     //  input virtual pixel
      py1 = Ycen + dy * s/d;
      vstat = vpixel(E1pxm,px1,py1,vpix);
      if (! vstat) continue;

      pix3 = PXMpix(E3pxm,px3,py3);                                              //  input --> output pixel
      memcpy(pix3,vpix,pcc);
   }

   return 0;                                                                     //  exit thread
}


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

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

namespace unwarp_closeup
{
   int         Fstarted;
   int         unwarp_area;
   float       warpval;
   int         centerX, centerY;
   editfunc    EFunwarp_closeup;
}


//  menu function

void m_unwarp_closeup(GtkWidget *, ch *menu)
{
   using namespace unwarp_closeup;

   int   unwarp_closeup_dialog_event(zdialog *zd, ch *event);
   void  unwarp_closeup_mousefunc();

   ch     *unwarp_closeup_message =
             " Use Select Area to select a face. \n"
             " Click on the center of distortion. \n"
             " Move the slider. \n";

   F1_help_topic = "unwarp closeup";

   Plog(1,"m_unwarp_closeup \n");

   EFunwarp_closeup.menufunc = m_unwarp_closeup;
   EFunwarp_closeup.menuname = "Unwarp Closeup";
   EFunwarp_closeup.Farea = 2;                                                   //  select area usable
   EFunwarp_closeup.mousefunc = unwarp_closeup_mousefunc;                        //  mouse function
   if (! edit_setup(EFunwarp_closeup)) return;                                   //  setup edit

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

***/

   zdialog *zd = zdialog_new("Unwarp Closeup",Mwin,"OK"," X ",null);
   EFunwarp_closeup.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",unwarp_closeup_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(unwarp_closeup_mousefunc,dragcursor);                               //  connect mouse function

   Fstarted = 0;
   unwarp_area = 0;

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


//  dialog event and completion callback function

int unwarp_closeup_dialog_event(zdialog * zd, ch *event)
{
   using namespace unwarp_closeup;

   void  unwarp_closeup_warpfunc();
   void  unwarp_closeup_mousefunc();

   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 != sa_stat_fini)                                                  //  no select area active
      init = 1;

   if (Fstarted && unwarp_area != areanumber)                                    //  select area changed
      init = 1;

   if (init) {
      Fstarted = 0;
      unwarp_area = 0;
      return 1;
   }

   if (strmatch(event,"focus"))                                                  //  reconnect mouse
      takeMouse(unwarp_closeup_mousefunc,dragcursor);

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

   return 1;
}


//  mouse function

void unwarp_closeup_mousefunc()
{
   using namespace unwarp_closeup;

   void  unwarp_closeup_warpfunc();

   if (LMclick) {
      centerX = Mxclick;                                                         //  capture central point
      centerY = Myclick;
   }
   else if (Mxdrag || Mydrag) {
      centerX = Mxdrag;
      centerY = Mydrag;
   }
   else return;

   LMclick = Mxdrag = Mydrag = 0;

   if (sa_stat != sa_stat_fini) {                                                //  no select area active
      zmessageACK(Mwin,"no active Select Area");
      Fstarted = 0;
      Mdrag = 0;
      return;
   }

   Fstarted = 1;                                                                 //  unwarp can proceed
   unwarp_area = areanumber;
   sa_edgecalc();                                                                //  calculate area edge distances

   unwarp_closeup_warpfunc();
   return;
}


//  warp image according to slider position

void unwarp_closeup_warpfunc()
{
   using namespace unwarp_closeup;

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

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

   hsize = sa_maxx - sa_minx;                                                    //  area size
   vsize = sa_maxy - sa_miny;

   cx = centerX;                                                                 //  unwarp center (nose tip)
   cy = centerY;

   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;
}


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

//  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 makewaves_names
{
   editfunc EFmakewaves;
   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_makewaves(GtkWidget *, ch *menu)
{
   using namespace makewaves_names;

   int    makewaves_dialog_event(zdialog* zd, ch *event);
   void * makewaves_thread(void *);

   F1_help_topic = "make waves";

   Plog(1,"m_makewaves \n");

   EFmakewaves.menufunc = m_makewaves;
   EFmakewaves.menuname = "Make Waves";
   EFmakewaves.Farea = 2;                                                        //  select area usable
   EFmakewaves.threadfunc = makewaves_thread;

   if (! edit_setup(EFmakewaves)) return;

/***
          ___________________________________
         |           Make waves              |
         |                                   |
         |             Horizontal  Vertical  |
         |  wavelength [___]        [___]    |
         |  amplitude  [___]        [___]    |
         |  variance   [___]        [___]    |
         |                                   |
         |  perspective [___]                |
         |                                   |
         |                  [Apply] [OK] [X] |
         |___________________________________|

***/

   zdialog *zd = zdialog_new("Make Waves",Mwin,"Apply","OK"," X ",null);
   EFmakewaves.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","wavelength","expand");
   zdialog_add_widget(zd,"label","labamp","vbw1","amplitude","expand");
   zdialog_add_widget(zd,"label","labamp","vbw1","variance","expand");
   zdialog_add_widget(zd,"label","labh","vbw2","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","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","perspective","space=3");
   zdialog_add_widget(zd,"zspin","persp","hbp","0|100|1|0","space=5");

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


//  makewaves dialog event and completion function

int makewaves_dialog_event(zdialog *zd, ch *event)
{
   using namespace makewaves_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);
         thread_signal();                                                        //  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 * makewaves_thread(void *)
{
   using namespace makewaves_names;

   void * makewaves_wthread(void *);

   do_wthreads(makewaves_wthread,NSMP);                                          //  worker threads

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

   Fpaint2();                                                                    //  update window
   return 0;
}


void * makewaves_wthread(void *arg)                                              //  working threads
{
   using namespace makewaves_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, *pix3, vpix[4];
   float    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 = Ehh;

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

   for (py = pylo + index; py < pyhi; py += NSMP)
   {
      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 < Eww; px++)
      {
         if (sa_stat == sa_stat_fini) {                                          //  select area active
            ii = py * Eww + 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];

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

   return 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;
   float       twist;
   float       center;
   float       rotate;
}


//  menu function

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

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

   ch       *title = "Twist";

   F1_help_topic = "twist";

   Plog(1,"m_twist \n");

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

   EFtwist.menufunc = m_twist;
   EFtwist.menuname = "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] [OK] [X] |
         |_________________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,"Reset","OK"," X ",null);
   EFtwist.zd = zd;

   zdialog_add_widget(zd,"label","labtip","dialog","Drag mouse to set center");

   zdialog_add_widget(zd,"hbox","hba","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","laba","hba","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","Center","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","Rotate","space=3");
   zdialog_add_widget(zd,"hscale","rotate","hbc","-4.0|4.0|0.01|0.0","space=5|expand");

   cx = Eww / 2;                                                                 //  initial data
   cy = Ehh / 2;
   twist = 0.0;
   center = 1.0;
   rotate = 0.0;

   thread_signal();

   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, ch *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();
      thread_signal();
      thread_wait();
      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);
         thread_signal();
      }

      else if (zd->zstat == 2) {                                                 //  done
         thread_wait();                                                          //  insure thread done
         float R = 1.0 * (E0pxm->ww + E0pxm->hh) / (Eww + Ehh);
         cx = R * cx;                                                            //  scale geometries to full size
         cy = R * cy;
         edit_fullsize();                                                        //  get full size image
         thread_signal();
         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,"twist")) {
      zdialog_fetch(zd,"twist",twist);
      thread_signal();
   }

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

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

   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;
   thread_signal();                                                              //  trigger image update

   LMclick = Mxdrag = Mydrag = 0;
   return;
}


//  thread function

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

   void * twist_wthread(void *arg);

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

   do_wthreads(twist_wthread,NSMP);                                              //  worker threads

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

   Fpaint2();
   return 0;
}


//  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;

   Dnw = cx * cx + cy * cy;                                                      //  get distance from center
   Dne = (Eww-cx) * (Eww-cx) + cy * cy;                                          //    to nearestest corner
   Dse = (Eww-cx) * (Eww-cx) + (Ehh-cy) * (Ehh-cy);
   Dsw = cx * cx + (Ehh-cy) * (Ehh - 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 < Ehh; py3 += NSMP)                                     //  loop all output pixels
   for (px3 = 0; px3 < Eww; 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;
      qy = 2.0 * qy + cy;
      vstat = vpixel(E1pxm,qx,qy,vpix);                                          //  input pixel

      px33 = px3 - (cx - Eww/2);                                                 //  move output image to center
      py33 = py3 - (cy - Ehh/2);
      if (px33 < 0 || px33 > Eww-1) continue;
      if (py33 < 0 || py33 > Ehh-1) continue;
      pix3 = PXMpix(E3pxm,px33,py33);                                            //  output pixel

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

   return 0;
}


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

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

namespace sphere_names
{
   int         Pww, Phh;                                                         //  preview 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 *, ch *menu)
{
   using namespace sphere_names;

   int    sphere_dialog_event(zdialog* zd, ch *event);
   void   sphere_mousefunc(void);
   void * sphere_thread(void *);

   ch     *title = "Spherical Projection";

   F1_help_topic = "sphere";

   Plog(1,"m_sphere \n");

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

   EFsphere.menufunc = m_sphere;
   EFsphere.menuname = "Sphere";
   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);

   Pww = Eww;                                                                    //  save preview dimensions
   Phh = Ehh;

   Xcen = Eww / 2;                                                               //  set dialog defaults
   Ycen = Ehh / 2;
   D1 = Eww;
   if (Ehh < Eww) D1 = Ehh;
   flatten = 0;
   magnify = 1.0;

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

***/

   zdialog *zd = zdialog_new(title,Mwin,"OK"," X ",null);                        //  sphere dialog
   CEF->zd = zd;

   zdialog_add_widget(zd,"label","labtip","dialog","Drag mouse to set center");
   zdialog_add_widget(zd,"hbox","hbflat","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labflat","hbflat","Flatten","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","Magnify","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
   thread_signal();                                                              //  trigger update thread
   return;
}


//  sphere dialog event and completion function

int sphere_dialog_event(zdialog *zd, ch *event)
{
   using namespace sphere_names;

   void sphere_mousefunc();

   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 (strmatch(event,"focus"))                                                  //  toggle mouse capture                  24.10
      takeMouse(sphere_mousefunc,dragcursor);

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  done
      {
         edit_fullsize();                                                        //  get full size image
         scale = 1.0 * (Eww + Ehh) / (Pww + Phh);                                //  scale up the parameters
         Xcen = scale * Xcen;
         Ycen = scale * Ycen;
         D1 = scale * D1;
         thread_signal();                                                        //  recalculate image
         edit_done(0);                                                           //  commit edit
      }

      else edit_cancel(0);                                                       //  discard edit

      return 1;
   }

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

   if (strmatch(event,"magnify")) {
      zdialog_fetch(zd,"magnify",magnify);
      thread_signal();                                                           //  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 * Eww || Mxposn > 0.9 * Eww) return;                         //  ignore if near image edge
   if (Myposn < 0.1 * Ehh || Myposn > 0.9 * Ehh) return;

   Xcen = Mxposn;                                                                //  new center
   Ycen = Myposn;

   edist = Xcen;                                                                 //  find nearest edge distance
   if (Ycen < edist) edist = Ycen;
   if (Eww - Xcen < edist) edist = Eww - Xcen;
   if (Ehh - Ycen < edist) edist = Ehh - Ycen;

   D1 = 2 * edist;                                                               //  new sphere diameter
   thread_signal();

   return;
}


//  thread function - multiple working threads to update image

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

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

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

   do_wthreads(sphere_wthread,NSMP);                                             //  worker threads

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

   Fpaint2();                                                                    //  update window
   return 0;
}


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 < Ehh; py3 += NSMP)                                     //  loop output pixels
   for (px3 = 0; px3 < Eww; 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 - Eww/2);                                               //  move to image center
      py33 = py3 - (Ycen - Ehh/2);
      if (px33 < 0 || px33 > Eww-1) continue;
      if (py33 < 0 || py33 > Ehh-1) continue;
      pix3 = PXMpix(E3pxm,px33,py33);                                            //  output pixel
      memcpy(pix3,vpix,pcc);
   }

   return 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         Pww, Phh, cx, cy, pixcc;
   float       R, T, hole;                                                       //  radius, theta
}


//  menu function

void m_inside_out(GtkWidget *, ch *menu)
{
   using namespace inside_out_names;

   int    inside_out_dialog_event(zdialog *zd, ch *event);
   void * inside_out_thread(void *);
   void   inside_out_mousefunc();

   F1_help_topic = "inside-out";

   Plog(1,"m_inside_out \n");

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

   EFinsideout.menuname = "Inside-out";
   EFinsideout.menufunc = m_inside_out;
   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);

   Pww = Eww;                                                                    //  previewdimensions
   Phh = Ehh;
   cx = Pww / 2;
   cy = Phh / 2;
   T = 0.0;
   hole = 100;

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

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

***/

   zdialog *zd = zdialog_new("Inside-out",Mwin,"OK"," X ",null);
   EFinsideout.zd = zd;

   zdialog_add_widget(zd,"label","labtip","dialog","Drag mouse to set center");
   zdialog_add_widget(zd,"hbox","hbrot","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labrot","hbrot","Rotate","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","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");
   thread_signal();
   return;
}


//  dialog event and completion callback function

int inside_out_dialog_event(zdialog *zd, ch *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);
      thread_signal();
   }

   if (strmatch(event,"hole")) {
      zdialog_fetch(zd,"hole",hole);
      thread_signal();
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  done
         thread_wait();                                                          //  insure thread done
         edit_fullsize();                                                        //  get full size image
         float S = 1.0 * Eww / Pww;
         cx = S * cx;                                                            //  scale params to full size
         cy = S * cy;
         hole = S * hole;
         thread_signal();
         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;
   thread_signal();                                                              //  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);

   R = cx;                                                                       //  R = minimum center - edge distance
   if (cy < cx) R = cy;
   if (Eww - cx < R) R = Eww - cx;
   if (Ehh - cy < R) R = Ehh - cy;

   if (R < 50) return 0;                                                         //  ignore absurd center

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

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

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

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


//  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 < Ehh; py3 += NSMP)                                     //  loop target pixels
   for (px3 = 0; px3 < Eww; px3++)
   {
      px33 = px3 - (cx - Eww/2);                                                 //  move to image center
      py33 = py3 - (cy - Ehh/2);

      if (px33 < 0 || px33 > Eww-1) continue;
      if (py33 < 0 || py33 > Ehh-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 > Eww-1) continue;
      if (py33 < 0 || py33 > Ehh-1) continue;

      memcpy(pix3,pix1,pixcc);
   }

   return 0;
}


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

//  Convert image to 'tiny planet' format.

namespace tiny_planet_names
{
   editfunc    EFtinyplanet;

   int      Pww, Phh;
   int      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 *, ch *menu)
{
   using namespace tiny_planet_names;

   int   tiny_planet_dialog_event(zdialog *zd, ch *event);
   void  tiny_planet_process();

   ch    texthole[20], texttop[20], textbott[20];

   F1_help_topic = "tiny planet";

   Plog(1,"m_tiny_planet \n");

   if (! curr_file) return;                                                      //  no current image

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

   EFtinyplanet.menuname = "Tiny Planet";
   EFtinyplanet.menufunc = m_tiny_planet;
   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);

   Pww = Eww;                                                                    //  preview dimensions
   Phh = Ehh;

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

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


        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,"OK"," X ",null);
   EFtinyplanet.zd = zd;

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

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

   snprintf(textbott,20,"0|%.0f|1|0", 0.3 * Ehh);                                //  cut-off bottom range
   zdialog_add_widget(zd,"hbox","hbbott","dialog");
   zdialog_add_widget(zd,"label","labcutbott","hbbott","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","Rotate","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","Reverse R","space=3");
   zdialog_add_widget(zd,"zbutton","revT","hbrev","Theta","space=8");

   hole = 0.1 * Ehh;                                                             //  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, ch *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
      {
         edit_fullsize();                                                        //  get full size input image
         float R = 1.0 * Eww / Pww;                                              //  scale params to full size
         cuttop = R * cuttop;
         cutbott = R * cutbott;
         hole = R * hole;
         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;
   uint     cc;
   float    *pix1, *pix9;

   void * tiny_planet_wthread(void *arg);

   ww9 = Eww;                                                                    //  make input image with cuttoffs
   hh9 = Ehh - 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, Eww * 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,NSMP);                                        //  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 += NSMP)                                     //  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);
   }

   return 0;
}


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

//  generate an image that repeats itself spiraling inward

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


//  menu function

void m_escher_spiral(GtkWidget *, ch *menu)
{
   using namespace escher_spiral_names;

   int    escher_spiral_dialog_event(zdialog *zd, ch *event);
   void * escher_spiral_thread(void *);
   void   escher_spiral_mousefunc();

   F1_help_topic = "escher spiral";

   Plog(1,"m_escher_spiral \n");

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

   EFescher_spiral.menuname = "Escher Spiral";
   EFescher_spiral.menufunc = m_escher_spiral;
   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

   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] [OK] [X] |
         |________________________________|

***/

   zdialog *zd = zdialog_new("Escher Spiral",Mwin,"Apply","OK"," X ",null);
   EFescher_spiral.zd = zd;

   zdialog_add_widget(zd,"label","labtip","dialog","Click mouse to change center");
   zdialog_add_widget(zd,"hbox","hbw","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labw","hbw","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","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","Color","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_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, ch *event)
{
   using namespace escher_spiral_names;

   void   escher_spiral_mousefunc();

   ch       text[20];
   ch       *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;
         thread_signal();
         return 1;
      }

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

   if (zstrstr("width rim color",event))
   {
      zdialog_fetch(zd,"width",width);
      zdialog_fetch(zd,"rim",rim);

      zdialog_fetch(zd,"color",text,20);
      pp = substring(text,"|",1);
      if (pp) color[0] = atoi(pp);
      pp = substring(text,"|",2);
      if (pp) color[1] = atoi(pp);
      pp = substring(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;

   thread_signal();
   return;
}


//  main thread function

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

   void  escher_spiral_function();

   escher_spiral_function();                                                     //  transform image

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

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


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, ii, *Npix;
   int      pcc = 3 * sizeof(float);
   uint     cc;
   int64    cc64;

   cc64 = imagesize(Eww,Ehh,pcc,1);
   memset(E3pxm->pixels,0,cc64);

   cc = Eww * Ehh * sizeof(int);                                                 //  clear dest. image pixel counts
   Npix = (int *) zmalloc(cc,"escher spiral");

   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;
}


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

//  Flatten a photo of a curved surface, e.g. a page in an open thick book.
//  Compensate for the curvature, make an image of the flattened surface.

editfunc    EFflatphoto;                                                         //  edit function data

namespace flatphoto {
   int      Tmx[20], Tmy[20], Bmx[20], Bmy[20];                                  //  upper/lower curve points
   int      Fupper, Flower;                                                      //  current curved line, upper or lower
   int      prad;                                                                //  point size on image (radius)
   int      Tnm = 0, Bnm = 0;                                                    //  upper/lower point counts
   int      Tbase, Bbase;                                                        //  upper/lower points, low values
   int      Eww1, Ehh1;                                                          //  original image dimensions
   int      Eww2;                                                                //  widened image width
   int      Ml, Mr;                                                              //  left and right crop margins
   int      Fcrop, Fwarp;                                                        //  flags, E3 already cropped, warped
   double   *Tfy, *Bfy;                                                          //  derived upper/lower y-shifts [hh]
   double   *Tfx, *Bfx;                                                          //  derived upper/lower x-shifts [ww]
   double   stretch;                                                             //  text stretch factor
   double   widen;                                                               //  image widen factor
}


//  menu function

void m_flatphoto(GtkWidget *, ch *menu)
{
   using namespace flatphoto;

   int    flatphoto_dialog_event(zdialog* zd, ch *event);
   void   flatphoto_mousefunc();
   void   flatphoto_drawlines();

   ch       *title = "Flatten curved surface. ";
   ch       *guide = "Trace upper and lower curves with \n"
                     " mouse clicks, then [flatten].";
   zdialog  *zd;

   F1_help_topic = "flatten photo";

   Plog(1,"m_flatphoto \n");

   EFflatphoto.menufunc = m_flatphoto;
   EFflatphoto.menuname = "Flatten Photo";
   EFflatphoto.mousefunc = flatphoto_mousefunc;

   if (! edit_setup(EFflatphoto)) return;                                        //  setup edit

   if (Eww != Eww1) Tnm = Bnm = 0;                                               //  clear prior points if image
   if (Ehh != Ehh1) Tnm = Bnm = 0;                                               //    size different from prior

   Eww1 = Eww;                                                                   //  save original image size
   Ehh1 = Ehh;
   
   prad = 1 + 0.001 * Eww;                                                       //  point size scaled to image size       24.30
   if (prad < 2) prad = 2;

   int cc = Eww * sizeof(double);                                                //  allocate memory
   Tfy = (double *) zmalloc(cc,"flat photo");
   Bfy = (double *) zmalloc(cc,"flat photo");
   Tfx = (double *) zmalloc(cc,"flat photo");
   Bfx = (double *) zmalloc(cc,"flat photo");

   Fcrop = Fwarp = 0;                                                            //  E3 not cropped or warped yet

/***
           _____________________________________
          |      Flatten curved surface         |
          |                                     |
          | Trace upper and lower curves with   |
          |  mouse clicks, then [flatten].      |
          |                                     |
          |  Trace  (o) upper  (o) lower        |                                //  24.30
          |                                     |
          |   [Clear] [Flatten] [Undo]          |
          |                                     |
          | Stretch: ========[]===============  |
          |  Widen:  =========[]==============  |
          |                                     |
          |                            [OK] [X] |
          |_____________________________________|

***/

   zd = zdialog_new(title,Mwin,"OK"," X ",null);                                 //  flatphoto dialog
   EFflatphoto.zd = zd;

   zdialog_add_widget(zd,"hbox","hbg","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labg","hbg",guide,"space=3");
   
   zdialog_add_widget(zd,"hbox","hbtr","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labtr","hbtr","Trace","space=5");
   zdialog_add_widget(zd,"radio","upper","hbtr","upper","space=5");
   zdialog_add_widget(zd,"radio","lower","hbtr","lower","space=10");
   
   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","clear","hbf","Clear","space=10");
   zdialog_add_widget(zd,"button","flatten","hbf","Flatten","space=10");
   zdialog_add_widget(zd,"button","undo","hbf","Undo","space=10");

   zdialog_add_widget(zd,"hbox","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbstr","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labstr","hbstr","Stretch:","space=8");
   zdialog_add_widget(zd,"hscale","stretch","hbstr","1|30|0.01|1","expand");

   zdialog_add_widget(zd,"hbox","hbwid","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labwid","hbwid","Widen:","space=8");
   zdialog_add_widget(zd,"hscale","widen","hbwid","1|2|0.01|1","expand");
   
   zdialog_stuff(zd,"upper",1);                                                  //  start with upper curve                24.30
   zdialog_stuff(zd,"lower",0);
   Fupper = 1;
   Flower = 0;

   if (Tnm > 0) zdialog_load_inputs(zd);                                         //  restore prior inputs if same size

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

   if (Tnm || Bnm) flatphoto_drawlines();                                        //  draw lines if points available

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


//  flatphoto dialog event and completion function

int flatphoto_dialog_event(zdialog *zd, ch *event)                               //  flatphoto dialog event function
{
   using namespace flatphoto;

   void flatphoto_drawlines();
   void flatphoto_crop();
   void flatphoto_warp();
   void flatphoto_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                  24.10
      takeMouse(flatphoto_mousefunc,dragcursor);

   if (zd->zstat) {
      zfree(Tfy);                                                                //  free memory
      zfree(Bfy);
      zfree(Tfx);
      zfree(Bfx);
      if (zd->zstat == 1 && Fwarp) edit_done(0);                                 //  commit edit                           24.10
      else edit_cancel(0);                                                       //  cancel
      return 1;
   }
   
   if (zstrstr("upper lower",event))                                             //  set current drawing curve             24.30
   {                                                                             //    upper or lower
      zdialog_fetch(zd,"upper",Fupper);
      zdialog_fetch(zd,"lower",Flower);
   }

   if (zstrstr("flatten stretch widen",event))                                   //  control widget changed
   {
      if (! Tnm && ! Bnm) {
         zmessageACK(Mwin,"No curve lines found");
         return 1;
      }
      
      if ((Tnm && Tnm < 4) || (Bnm && Bnm < 4)) {
         zmessageACK(Mwin,"Curve lines need at least 4 points");
         return 1;
      }

      if (! Tnm) {                                                               //  upper curve missing, create           24.30
         Tmx[0] = prad;
         Tmx[1] = 0.33 * Eww1;
         Tmx[2] = 0.67 * Eww1;
         Tmx[3] = Eww1-prad-1;
         Tmy[0] = Tmy[1] = Tmy[2] = Tmy[3] = prad;
         Tnm = 4;
      }

      if (! Bnm) {                                                               //  lower curve missing, create           24.30
         Bmx[0] = prad;
         Bmx[1] = 0.33 * Eww1;
         Bmx[2] = 0.67 * Eww1;
         Bmx[3] = Eww1-prad-1;
         Bmy[0] = Bmy[1] = Bmy[2] = Bmy[3] = Ehh1-prad-1;
         Bnm = 4;
      }

      zdialog_fetch(zd,"stretch",stretch);
      zdialog_fetch(zd,"widen",widen);

      flatphoto_crop();                                                          //  crop image
      flatphoto_warp();                                                          //  flatten image
   }

   if (strmatch(event,"clear")) {                                                //  undo all changes, clear points
      Tnm = Bnm = 0;
      edit_undo();
      Fcrop = Fwarp = 0;
      Eww = Eww1;
   }

   if (strmatch(event,"undo")) {                                                 //  undo all changed, keep points
      edit_reset();
      Fcrop = Fwarp = 0;
      Eww = Eww1;
      flatphoto_drawlines();
   }

   if (strmatch(event,"line_color")) flatphoto_drawlines();                      //  refresh lines after color change

   return 1;
}


//  flatphoto mouse function
//  save clicked image points

void flatphoto_mousefunc()
{
   using namespace flatphoto;

   void flatphoto_drawlines();

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

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

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

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

   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;
   }

   if (Fupper) {
      for (ii = 0; ii < Tnm; ii++) {                                             //  compare mouse position with
         dist = abs(mx-Tmx[ii]);                                                 //    existing upper curve 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];
            }
         }
      }
   }

   if (Flower) {
      for (ii = 0; ii < Bnm; ii++) {                                             //  same for lower curve 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 (Fupper) {                                                              //  add to upper curve 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++;
      }

      if (Flower) {                                                              //  add to lower curve 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++;
      }
   }

   flatphoto_drawlines();                                                        //  draw curve through points
   return;
}


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

void flatphoto_drawlines()
{
   using namespace flatphoto;

   void flatphoto_drawpoint(int px, int py, int R);

   float    Tnx[20], Tny[20], Bnx[20], Bny[20];                                  //  upper/lower spline nodes
   int      topmax, topmin, bottmax, bottmin;                                    //  limits of image update areas
   int      ii, px, py, qx, qy, npq;
   int      R = prad/2;

   if (Fwarp) return;                                                            //  E3 image warped

   edit_reset(); 

   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 upper curve 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 > Eww-1) continue;
         if (qy < 0 || qy > Ehh-1) continue;
         flatphoto_drawpoint(qx,qy,R);
         if (qy > topmax) topmax = qy;
         if (qy < topmin) topmin = qy;
      }
   }

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

   for (ii = 0; ii < Bnm; ii++)                                                  //  draw lower curve 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 > Eww-1) continue;
         if (qy < 0 || qy > Ehh-1) continue;
         flatphoto_drawpoint(qx,qy,R);
         if (qy > bottmax) bottmax = qy;
         if (qy < bottmin) bottmin = qy;
      }
   }

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

      for (ii = 0; ii < Tnm; ii++) {                                             //  copy upper 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 < Eww; px++)                                               //  generate upper curve y shifts
         Tfy[px] = spline2(px);

      for (px = Tmx[0]; px < Tmx[Tnm-1]; px++) {                                 //  draw upper curve over image
         py = Tfy[px] + Tbase;
         if (py < 0 || py > Ehh-1) continue;
         flatphoto_drawpoint(px,py,R);
         if (py < topmin) topmin = py;
         if (py > topmax) topmax = py;
      }
   }

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

      for (ii = 0; ii < Bnm; ii++) {                                             //  copy lower 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 < Eww; px++)                                               //  generate lower curve y shifts
         Bfy[px] = spline2(px);

      for (px = Bmx[0]; px < Bmx[Bnm-1]; px++) {                                 //  draw lower curve over image
         py = Bfy[px] + Bbase;
         if (py < 0 || py > Ehh-1) continue;
         flatphoto_drawpoint(px,py,R);
         if (py < bottmin) bottmin = py;
         if (py > bottmax) bottmax = py;
      }
   }

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

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

   return;
}


//  draw one fat point on image

void flatphoto_drawpoint(int px, int py, int R)                                  //  24.30
{
   using namespace flatphoto;

   float    *pix;
   int      qx, qy, nn;

   if (px < R) px = R;
   if (px > Eww-R-1) px = Eww-R-1;
   if (py < R) py = R;
   if (py > Ehh-R-1) py = Ehh-R-1;

   for (qy = py-R; qy <= py+R; qy++)
   for (qx = px-R; qx <= px+R; qx++)
   {
      pix = PXMpix(E3pxm,qx,qy);
      pix[0] = LINE_COLOR[0];
      pix[1] = LINE_COLOR[1];
      pix[2] = LINE_COLOR[2];
   }
   
   nn = 2 * R + 1;
   Fpaint3(px-R,py-R,nn,nn,0); 
   return;
}


//  crop image left and right margins using 4 outermost mouse points

void flatphoto_crop()
{
   using namespace flatphoto;

   int      ii, ww;

   if (Fcrop) return;                                                            //  already cropped

   if (Tmx[0] < Bmx[0]) Ml = Tmx[0];                                             //  left margin
   else  Ml = Bmx[0];

   if (Tmx[Tnm-1] > Bmx[Bnm-1]) Mr = Tmx[Tnm-1];                                 //  right margin
   else  Mr = Bmx[Bnm-1];

   ww = Mr - Ml;                                                                 //  rectangle width

   E9pxm = PXM_copy_area(E1pxm,Ml,0,ww,Ehh);                                     //  E9 = cropped E1
   PXM_free(E3pxm);
   E3pxm = PXM_copy(E9pxm);                                                      //  E9 preserves E3 for use by warp()
   Eww = ww;                                                                     //  new E3 image width

   for (ii = 0; ii < Eww; ii++)                                                  //  shift upper/lower curve points
   {                                                                             //    for removed left margin
      Tfy[ii] = Tfy[ii+Ml];
      Bfy[ii] = Bfy[ii+Ml];
   }

   Fcrop = 1;                                                                    //  crop was done

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

   Fpaint2();                                                                    //  update window

   return;
}


//  warp image based on upper and lower curves

void flatphoto_warp()
{
   using namespace flatphoto;

   int      px, py, vstat;
   int      d1, d2, d3, d4, vl, vr;
   double   Tslope, Bslope, slope;
   double   Rt, Rb, Rl, Rr;
   float    *pix3, vpix[4], vpx, vpy;
   int      nc = E1pxm->nc, pcc = nc * sizeof(float);
   PXM      *tempxm;

   Funcbusy(+1);                                                                 //  24.10
   zmainsleep(0.1);

   PXM_free(E3pxm);
   E3pxm = PXM_copy(E9pxm);

   d1 = Tmx[0] - Ml;                                                             //  NW  gaps at 4 corners
   d2 = Eww - (Tmx[Tnm-1] - Ml);                                                 //  NE   from page rotation
   d3 = Bmx[0] - Ml;                                                             //  SW
   d4 = Eww - (Bmx[Bnm-1] - Ml);                                                 //  SE

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

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

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

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

   Rt = (Eww-1) / Tfx[Eww-1];                                                    //  make overall width the same
   Rb = (Eww-1) / Bfx[Eww-1];

   for (px = 0; px < Eww; px++) {
      Tfx[px] = Rt * Tfx[px];                                                    //  using separate loops causes
      Bfx[px] = Rb * Bfx[px];                                                    //      GCC optimization bug
   }

   for (py = 0; py < Ehh; py++)                                                  //  loop from upper to lower
   {
      Rt = 1.0 - 1.0 * py / Ehh;                                                 //  Rt from 1 to 0  - upper weight
      Rb = 1.0 - Rt;                                                             //  Rb from 0 to 1  - lower weight

      vl = d1 * Rt + d3 * Rb;                                                    //  left gap
      vr = d2 * Rt + d4 * Rb;                                                    //  right gap

      for (px = 0; px < Eww; px++)                                               //  loop from left to right
      {
         Rl = 1.0 - 1.0 * px / Eww;                                              //  Rl from 1 to 0  - left weight
         Rr = 1.0 - Rl;                                                          //  Rr from 0 to 1  - right weight

         vpx = 0.5 * Tfx[px] + 0.5 * Bfx[px];                                    //  use upper/lower stretch equally

         vpx += Rl * vl - Rr * vr;                                               //  shift to cancel gaps

         vpy = py + Rt * Tfy[int(vpx)] + Rb * Bfy[int(vpx)];                     //  flatten upper/lower curved lines

         vstat = vpixel(E9pxm,vpx,vpy,vpix);                                     //  get source pixel

         pix3 = PXMpix(E3pxm,px,py);                                             //  E3 output pixel
         if (vstat) memcpy(pix3,vpix,pcc);
         else memset(pix3,0,pcc);                                                //  off page, void pixel
      }
   }

   if (widen > 1.0) {                                                            //  widen image
      Eww2 = Eww * widen;
      tempxm = PXM_rescale(E3pxm,Eww2,Ehh);
      PXM_free(E3pxm);
      E3pxm = tempxm;
   }

   Funcbusy(-1);

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

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

   Fpaint2();                                                                    //  update window

   return;
}
