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

   Fotoxx      edit photos and manage collections

   Copyright 2007 2008 2009 2010 2011 2012 Michael Cornelison
   Source URL: http://kornelix.squarespace.com/fotoxx
   Contact: kornelix2@googlemail.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.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program. If not, see http://www.gnu.org/licenses/.

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

#define EX extern                                                          //  enable extern declarations
#include "fotoxx.h"


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

   Fotoxx image edit - retouch functions

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


//  brightness / color / contrast adjustment

void  tune_curve_update(int spc);                                          //  curve update callback function
void  tune_update_image();                                                 //  process all pixels
void  tune_update_pixel(int px, int py);                                   //  process one pixel
void  *tune_thread(void *);                                                //  thread process for all pixels

int      tune_spc;                                                         //  current spline curve 1-5
int      tune_spc_moved[6];                                                //  tracks which curves changed

double   tune_pow[10000];                                                  //  pow(2,x) for x = -5.0 to +5.0
int      tune_ftf = 1;

editfunc    EFtune;


void m_tune(GtkWidget *, cchar *)                                          //  menu function
{
   int   tune_dialog_event(zdialog *zd, cchar *event);

   cchar    *title = ZTX("Adjust Brightness and Color");
   zfuncs::F1_help_topic = "tune";                                         //  v.10.8
   
   EFtune.funcname = "bright-color";
   EFtune.Fprev = 1;                                                       //  use preview
   EFtune.Farea = 2;                                                       //  select area usable
   EFtune.Fpara = 1;                                                       //  parallel edit OK
   EFtune.threadfunc = tune_thread;
   if (! edit_setup(EFtune)) return;                                       //  setup edit
   
   if (tune_ftf) {
      tune_ftf = 0;                                                        //  pre-compute pow(2,x)
      for (int ii = 0; ii < 10000; ii++)                                   //   for x = -5.0 to +4.999
         tune_pow[ii] = pow(2,(ii/1000.0 - 5.0));
   }
   
/***
            Adjust Brightness and Color
       ____________________________________________
      |                                            |
      |                                            |
      |           curve drawing area               |
      |                                            |
      |                                            |
      |____________________________________________|
       darker areas                   lighter areas

      [+++] [---] [+ -] [- +] [+-+] [-+-]
      [x] small-steps  Curve File: [ Open ] [ Save ]
      (o) brightness         [reset 1]  [reset all]
      (o) color saturation   [histogram]
      (o) red
      (o) green
      (o) blue
                                 [done] [cancel]
***/

   zdialog *zd = zdialog_new(title,mWin,Bdone,Bcancel,null);
   EFtune.zd = zd;

   zdialog_add_widget(zd,"frame","fr1","dialog",0,"expand");
   zdialog_add_widget(zd,"hbox","hba","dialog");
   zdialog_add_widget(zd,"label","labda","hba",Bdarker,"space=5");
   zdialog_add_widget(zd,"label","space","hba",0,"expand");
   zdialog_add_widget(zd,"label","labba","hba",Blighter,"space=5");

   zdialog_add_widget(zd,"hbox","hbb","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","b +++","hbb","+++");
   zdialog_add_widget(zd,"button","b ---","hbb","‒ ‒ ‒");
   zdialog_add_widget(zd,"button","b +-", "hbb"," + ‒ ");
   zdialog_add_widget(zd,"button","b -+", "hbb"," ‒ + ");
   zdialog_add_widget(zd,"button","b +-+","hbb","+ ‒ +");
   zdialog_add_widget(zd,"button","b -+-","hbb","‒ + ‒");

   zdialog_add_widget(zd,"hbox","hbcf","dialog",0,"space=5");
   zdialog_add_widget(zd,"check","smallstep","hbcf",ZTX("small-steps"));
   zdialog_add_widget(zd,"label","labcf","hbcf",Bcurvefile,"space=5");
   zdialog_add_widget(zd,"button","load","hbcf",Bopen);
   zdialog_add_widget(zd,"button","save","hbcf",Bsave,"space=5");

   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"vbox","vb21","hb2");
   zdialog_add_widget(zd,"vbox","vb22","hb2");
   zdialog_add_widget(zd,"radio","radbri","vb21",Bbrightness);
   zdialog_add_widget(zd,"radio","radsat","vb21",ZTX("color saturation"));
   zdialog_add_widget(zd,"radio","radR","vb21",Bred);
   zdialog_add_widget(zd,"radio","radG","vb21",Bgreen);
   zdialog_add_widget(zd,"radio","radB","vb21",Bblue);

   zdialog_add_widget(zd,"hbox","hbrs","vb22",0,"space=5");
   zdialog_add_widget(zd,"label","space","hbrs",0,"space=10");
   zdialog_add_widget(zd,"button","reset1","hbrs",ZTX(" reset 1 "));
   zdialog_add_widget(zd,"button","resetA","hbrs",ZTX("reset all"));
   zdialog_add_widget(zd,"hbox","hbhist","vb22",0,"space=6");
   zdialog_add_widget(zd,"label","space","hbhist",0,"space=10");
   zdialog_add_widget(zd,"button","histo","hbhist",Bhistogram);
   
   GtkWidget *frame = zdialog_widget(zd,"fr1");                            //  setup for curve editing
   spldat *sd = splcurve_init(frame,tune_curve_update);                    //  v.11.01
   EFtune.curves = sd;
   
   sd->Nscale = 3;                                                         //  y-scale lines at 1.0 EV intervals
   sd->xscale[0][0] = 0.00;                                                //  x and y values      v.11.10
   sd->yscale[0][0] = 0.25;
   sd->xscale[1][0] = 1.00;
   sd->yscale[1][0] = 0.25;
   sd->xscale[0][1] = 0.00;
   sd->yscale[0][1] = 0.50;
   sd->xscale[1][1] = 1.00;
   sd->yscale[1][1] = 0.50;
   sd->xscale[0][2] = 0.00;
   sd->yscale[0][2] = 0.75;
   sd->xscale[1][2] = 1.00;
   sd->yscale[1][2] = 0.75;

   for (int spc = 0; spc < 6; spc++)                                       //  setup 6 spline curves
   {                                                                       //  no. 0 is active curve
      sd->vert[spc] = 0;                                                   //  all curves are horizontal
      sd->nap[spc] = 3;                                                    //  curves 1-6 are copied to 0 when active
      sd->apx[spc][0] = 0.01;
      sd->apy[spc][0] = 0.5;
      sd->apx[spc][1] = 0.5;
      sd->apy[spc][1] = 0.5;
      sd->apx[spc][2] = 0.99;
      sd->apy[spc][2] = 0.5;
      splcurve_generate(sd,spc);
      tune_spc_moved[spc] = 0;
   }

   sd->Nspc = 1;                                                           //  only one at a time is active
   tune_spc = 1;                                                           //  default curve = brightness

   zdialog_stuff(zd,"radbri",1);                                           //  stuff default selection
   
   zdialog_resize(zd,0,500);
   zdialog_help(zd,"tune");                                                //  zdialog help topic        v.11.08
   zdialog_run(zd,tune_dialog_event,"save");                               //  run dialog - parallel     v.11.07
   return;
}


//  dialog event and completion callback function

int tune_dialog_event(zdialog *zd, cchar *event)
{
   int         Fupdate = 0;
   int         ii, jj, nn;
   static int  smallstep = 0;
   double      step, step2, step4;
   double      px, py;
   spldat      *sd = EFtune.curves;
   spldat      sdtemp;
   
   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(EFtune);                               //  done
      else edit_cancel(EFtune);                                            //  cancel or destroy
      return 0;
   }

   edit_takeover(EFtune);                                                  //  set my edit function

   if (strEqu(event,"reset"))                                              //  reset dialog controls     v.11.08
      event = "resetA";

   if (strEqu(event,"blendwidth")) tune_update_image();                    //  v.10.3
   if (strEqu(event,"histo")) m_histogram(0,0);                            //  popup brightness histogram

   if (strnEqu(event,"rad",3)) {                                           //  new choice of curve
      ii = strcmpv(event,"radbri","radsat","radR","radG","radB",null);
      tune_spc = ii;
      sd->nap[0] = sd->nap[ii];                                            //  copy active curve to curve 0
      for (jj = 0; jj < sd->nap[0]; jj++) {
         sd->apx[0][jj] = sd->apx[ii][jj];
         sd->apy[0][jj] = sd->apy[ii][jj];
      }
      Fupdate++;
   }
   
   if (strEqu(event,"smallstep"))                                          //  toggle [+++] etc. step size
      zdialog_fetch(zd,"smallstep",smallstep);

   ii = tune_spc;                                                          //  current active curve

   if (strnEqu(event,"b ",2)) {                                            //  button [+++] etc. - move entire curve
      for (jj = 0; jj < sd->nap[ii]; jj++) {
         px = sd->apx[0][jj];
         py = sd->apy[0][jj];

         step = 0.025;                                                     //  1/40
         if (smallstep) step = step * 0.3333;
         step2 = 2 * step;
         step4 = 4 * step;

         if (strEqu(event,"b +++")) py += step;                            //  range is 40 steps         v.11.07
         if (strEqu(event,"b ---")) py -= step;                            //   = -2.0 to +2.0 EV
         if (strEqu(event,"b +-"))  py += step - step2 * px;               //  normal steps are 0.1 EV or 0.03 OD
         if (strEqu(event,"b -+"))  py -= step - step2 * px;               //  small steps are 1/3 as big
         if (strEqu(event,"b +-+")) py -= step - step4 * fabs(px-0.5);
         if (strEqu(event,"b -+-")) py += step - step4 * fabs(px-0.5);
         if (py > 1) py = 1;
         if (py < 0) py = 0;
         sd->apy[0][jj] = py;
      }
      tune_spc_moved[ii] = 1;
      Fupdate++;
   }
   
   if (strEqu(event,"reset1")) {                                           //  reset current curve
      sd->nap[0] = 3;
      sd->apx[0][0] = 0.01;                                                //  3 anchor points, flatline
      sd->apy[0][0] = 0.5;
      sd->apx[0][1] = 0.5;
      sd->apy[0][1] = 0.5;
      sd->apx[0][2] = 0.99;
      sd->apy[0][2] = 0.5;
      Fupdate++;
      tune_spc_moved[ii] = 0;
   }
   
   if (strEqu(event,"resetA")) 
   {
      for (jj = 0; jj < 6; jj++) {                                         //  reset all curves
         sd->nap[jj] = 3;
         sd->apx[jj][0] = 0.01;
         sd->apy[jj][0] = 0.5;
         sd->apx[jj][1] = 0.5;
         sd->apy[jj][1] = 0.5;
         sd->apx[jj][2] = 0.99;
         sd->apy[jj][2] = 0.5;
         splcurve_generate(sd,jj);                                         //  regenerate all
         tune_spc_moved[jj] = 0;
      }
      Fupdate++;
   }

   if (strEqu(event,"load"))                                               //  load 5 saved curves    v.11.02
   {
      sdtemp.Nspc = 5;
      sdtemp.drawarea = 0;
      nn = splcurve_load(&sdtemp);
      if (nn != 1) return 0;

      for (ii = 0; ii < 5; ii++) {
         for (jj = 0; jj < sdtemp.nap[ii]; jj++) {
            sd->apx[ii+1][jj] = sdtemp.apx[ii][jj];
            sd->apy[ii+1][jj] = sdtemp.apy[ii][jj];
         }
         splcurve_generate(sd,ii+1);
         tune_spc_moved[ii+1] = 1;
      }

      ii = tune_spc;
      sd->nap[0] = sd->nap[ii];                                            //  copy active curve to curve 0
      for (jj = 0; jj < sd->nap[0]; jj++) {
         sd->apx[0][jj] = sd->apx[ii][jj];
         sd->apy[0][jj] = sd->apy[ii][jj];
      }
      Fupdate++;
   }

   if (strEqu(event,"save"))                                               //  save 5 curves to file   v.11.02
   {
      sdtemp.Nspc = 5;
      for (ii = 0; ii < 5; ii++) {
         sdtemp.nap[ii] = sd->nap[ii+1];
         for (jj = 0; jj < sd->nap[ii+1]; jj++) {
            sdtemp.apx[ii][jj] = sd->apx[ii+1][jj];
            sdtemp.apy[ii][jj] = sd->apy[ii+1][jj];
         }
      }
      splcurve_save(&sdtemp);
   }

   if (Fupdate)                                                            //  curve has changed
   {
      splcurve_generate(sd,0);                                             //  regenerate curve 0
      
      ii = tune_spc;                                                       //  active curve
      sd->nap[ii] = sd->nap[0];                                            //  copy curve 0 to active curve
      for (jj = 0; jj < sd->nap[0]; jj++) {
         sd->apx[ii][jj] = sd->apx[0][jj];
         sd->apy[ii][jj] = sd->apy[0][jj];
      }
      for (jj = 0; jj < 1000; jj++)
         sd->yval[ii][jj] = sd->yval[0][jj];

      splcurve_draw(0,0,sd);                                               //  draw curve
      tune_update_image();                                                 //  trigger image update
   }
   
   return 1;
}


//  this function is called when curve 0 is edited using mouse

void  tune_curve_update(int)
{
   int      ii = tune_spc, jj;
   spldat   *sd = EFtune.curves;

   edit_takeover(EFtune);                                                  //  set my edit function

   sd->nap[ii] = sd->nap[0];                                               //  copy curve 0 to current curve
   for (jj = 0; jj < sd->nap[0]; jj++) {
      sd->apx[ii][jj] = sd->apx[0][jj];
      sd->apy[ii][jj] = sd->apy[0][jj];
   }
   for (jj = 0; jj < 1000; jj++)
      sd->yval[ii][jj] = sd->yval[0][jj];

   tune_spc_moved[ii] = 1;
   tune_update_image();                                                    //  update image
   return;
}


//  Update image based on latest settings of all dialog controls.

void tune_update_image()                                                   //  v.11.06
{
   int      px, py, ww, hh;

   if (Factivearea && sa_Npixel < 200000)                                  //  if relatively small area,
   {                                                                       //    process in main() thread   v.11.06
      for (py = sa_miny; py < sa_maxy; py++)
      for (px = sa_minx; px < sa_maxx; px++)
         tune_update_pixel(px,py);

      px = sa_minx;                                                        //  direct window update
      py = sa_miny;
      ww = sa_maxx - sa_minx;
      hh = sa_maxy - sa_miny;
      mwpaint3(px,py,ww,hh);                                               //  speedup    v.11.06
      CEF->Fmod = 1;
   }
   
   else signal_thread();                                                   //  else use thread process
   return;
}


//  Update image based on latest settings of all dialog controls.

void * tune_thread(void *)
{
   void * tune_wthread(void *arg);
   
   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request
      
      for (int ii = 0; ii < Nwt; ii++)                                     //  start worker threads
         start_wthread(tune_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion

      CEF->Fmod = 1;
      mwpaint2();                                                          //  update window
   }

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


void * tune_wthread(void *arg)                                             //  worker thread function
{
   int      px, py;
   int      index = *((int *) arg);

   for (py = index; py < E1hh; py += Nwt)
   for (px = 0; px < E1ww; px++)
      tune_update_pixel(px,py);

   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


void tune_update_pixel(int px, int py)                                     //  process one pixel
{
   int         ii, kk, dist = 0;
   uint16      *pix1, *pix3;
   double      red1, green1, blue1, red3, green3, blue3;
   double      xval, brmax, brmean, brout, brightness;
   spldat      *sd = EFtune.curves;
   
   if (Factivearea) {                                                      //  select area active
      ii = py * Fww + px;
      dist = sa_pixmap[ii];                                                //  distance from edge
      if (! dist) return;                                                  //  pixel outside area
   }

   pix1 = PXMpix(E1pxm16,px,py);                                           //  input pixel
   pix3 = PXMpix(E3pxm16,px,py);                                           //  output pixel
   
   red1 = red3 = pix1[0];                                                  //  input and output RGB values
   green1 = green3 = pix1[1];
   blue1 = blue3 = pix1[2];

   brightness = 0.25 * red3 + 0.65 * green3 + 0.10 * blue3;                //  perceived brightness   v.10.9
   xval = brightness / 65536.0;                                            //  curve x-value, 0 to 0.999

/* ------------------------------------------------------------------------

      brightness adjustment

      brightness curve values:
           0 = dark
         0.5 = normal, unchanged
         1.0 = 200% brightness, clipped
*/

   if (tune_spc_moved[1])                                                  //  curve has been edited
   {
      kk = 1000 * xval;                                                    //  x-value          speedup v.11.06
      if (kk > 999) kk = 999;
      brout = sd->yval[1][kk];                                             //  y-value, 0 to 1.0
      brout = 4 * brout - 2;                                               //  F-stops, -2 to +2         v.11.07
      brout = tune_pow[int(1000*brout+5000.5)];                            //  0.25 to 4.0
      
      brmax = red3;                                                        //  brmax = brightest color
      if (green3 > brmax) brmax = green3;
      if (blue3 > brmax) brmax = blue3;

      red3 = red3 * brout;                                                 //  apply to all colors
      green3 = green3 * brout;                                             //  remove limit check        v.10.9
      blue3 = blue3 * brout;                                               //  (result is better)
   }
      
/* ------------------------------------------------------------------------

      color saturation curve values:
           0 = no color saturation (gray scale)
         0.5 = normal (initial unmodified RGB)
         1.0 = max. color saturation

      0.5 >> 0:  move all RGB values to their mean: (R+G+B)/3
      0.5 >> 1.0:  increase RGB spread to 2x original level
*/

   if (tune_spc_moved[2])                                                  //  curve has been edited
   {
      kk = 1000 * xval;                                                    //  speedup    v.11.06
      if (kk > 999) kk = 999;
      brout = sd->yval[2][kk];                                             //  saturation factor, 0 - 1
      brout = 2.0 * brout - 1.0;                                           //  -1.0 .. +1.0
      brmean = 0.333 * (red3 + green3 + blue3);
      red3 = red3 + brout * (red3 - brmean);                               //  simplified                v.10.9
      green3 = green3 + brout * (green3 - brmean);
      blue3 = blue3 + brout * (blue3 - brmean);
      if (red3 < 0) red3 = 0;
      if (green3 < 0) green3 = 0;
      if (blue3 < 0) blue3 = 0;
   }

/* ------------------------------------------------------------------------

      color balance curve values:
         0 = 0.5 * original color
         0.5 = unmodified
         1.0 = 1.5 * original color, clipped
*/

   if (tune_spc_moved[3]) {                                                //  curve has been edited
      kk = 1000 * xval;                                                    //  speedup    v.11.06
      if (kk > 999) kk = 999;
      brout = sd->yval[3][kk];                                             //  0 to 1.0
      brout = 4 * brout - 2;                                               //  F-stops, -2 to +2         v.11.07
      brout = tune_pow[int(1000*brout+5000.5)];                            //  0.25 to 4.0
      red3 = red3 * brout;
   }

   if (tune_spc_moved[4]) {
      kk = 1000 * xval;
      if (kk > 999) kk = 999;
      brout = sd->yval[4][kk];
      brout = 4 * brout - 2;
      brout = tune_pow[int(1000*brout+5000.5)];
      green3 = green3 * brout;
   }

   if (tune_spc_moved[5]) {
      kk = 1000 * xval;
      if (kk > 999) kk = 999;
      brout = sd->yval[5][kk];
      brout = 4 * brout - 2;
      brout = tune_pow[int(1000*brout+5000.5)];
      blue3 = blue3 * brout;
   }

//  if working within a select area, blend changes over distance from edge
      
   double      dold, dnew;

   if (Factivearea && dist < sa_blend) {
      dnew = 1.0 * dist / sa_blend;
      dold = 1.0 - dnew;
      red3 = dnew * red3 + dold * red1;
      green3 = dnew * green3 + dold * green1;
      blue3 = dnew * blue3 + dold * blue1;
   }

//  prevent overflow and set output RGB values

   double      cmax;

   cmax = red3;                                                            //  detect overflow        v.11.07
   if (green3 > cmax) cmax = green3;
   if (blue3 > cmax) cmax = blue3;
   
   if (cmax > 65535) {                                                     //  stop overflow
      red3 = red3 * 65535 / cmax;
      green3 = green3 * 65535 / cmax;
      blue3 = blue3 * 65535 / cmax;
   }

   pix3[0] = int(red3);
   pix3[1] = int(green3);
   pix3[2] = int(blue3);
   
   return;
}


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

//  adjust image gamma using classic gamma curve

editfunc    EFgamma;
int         gamma_spc;                                                     //  current spline curve

void m_gamma(GtkWidget *, cchar *)                                         //  new v.11.10
{
   int    gamma_dialog_event(zdialog* zd, cchar *event);
   void   gamma_curvedit(int spc);
   void * gamma_thread(void *);

   zfuncs::F1_help_topic = "gamma_curve";

   EFgamma.funcname = "gamma";
   EFgamma.Fprev = 1;                                                      //  use preview
   EFgamma.Farea = 2;                                                      //  select area usable
   EFgamma.Fpara = 1;                                                      //  parallel edit OK
   EFgamma.threadfunc = gamma_thread;
   if (! edit_setup(EFgamma)) return;                                      //  setup edit
   
/***
          _________________________________________
         |                                         |                       //  5 curves are maintained:
         |                                         |                       //  curve 0: current display curve
         |                                         |                       //        1: curve for all colors
         |                                         |                       //        2,3,4: red, green, blue
         |           curve edit area               |
         |                                         |
         |                                         |
         |                                         |
         |_________________________________________|

            (o) all  (o) red  (o) green  (o) blue                          //  colors: all, red, green, blue
            Curve File: [Open] [Save]   [histogram]
                                   [Done]  [Cancel]    
***/

   zdialog *zd = zdialog_new(ZTX("adjust image gamma"),mWin,Bdone,Bcancel,null);
   EFgamma.zd = zd;

   zdialog_add_widget(zd,"frame","frame","dialog",0,"expand");
   zdialog_add_widget(zd,"hbox","hbrgb","dialog",0,"space=3");
   zdialog_add_widget(zd,"radio","all","hbrgb",Ball,"space=5");
   zdialog_add_widget(zd,"radio","red","hbrgb",Bred,"space=5");
   zdialog_add_widget(zd,"radio","green","hbrgb",Bgreen,"space=5");
   zdialog_add_widget(zd,"radio","blue","hbrgb",Bblue,"space=5");
   zdialog_add_widget(zd,"hbox","hbcf","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labcf","hbcf",Bcurvefile,"space=5");
   zdialog_add_widget(zd,"button","load","hbcf",Bopen);
   zdialog_add_widget(zd,"button","save","hbcf",Bsave,"space=3");
   zdialog_add_widget(zd,"button","hist","hbcf",Bhistogram,"space=15");
   

   GtkWidget *frame = zdialog_widget(zd,"frame");
   spldat *sd = splcurve_init(frame,gamma_curvedit);
   EFgamma.curves = sd;

   sd->Nscale = 1;                                                         //  diagonal fixed line for neutral curve
   sd->xscale[0][0] = 0.00;
   sd->yscale[0][0] = 0.00;
   sd->xscale[1][0] = 1.00;
   sd->yscale[1][0] = 1.00;

   for (int ii = 0; ii < 5; ii++)                                          //  loop curves 0-4
   {
      sd->nap[ii] = 3;                                                     //  initial curves are flat
      sd->vert[ii] = 0;
      sd->apx[ii][0] = sd->apy[ii][0] = 0.01;
      sd->apx[ii][1] = sd->apy[ii][1] = 0.50;
      sd->apx[ii][2] = sd->apy[ii][2] = 0.99;
      splcurve_generate(sd,ii);
   }

   sd->Nspc = 1;                                                           //  only curve 0 is shown 
   gamma_spc = 1;                                                          //  current active curve: 1 (all)
   
   zdialog_stuff(zd,"all",1);                                              //  stuff default selection, all

   zdialog_resize(zd,260,300);
   zdialog_help(zd,"gamma_curve");                                         //  zdialog help topic 
   zdialog_run(zd,gamma_dialog_event,"save");                              //  run dialog - parallel

   return;
}


//  dialog event and completion callback function

int gamma_dialog_event(zdialog *zd, cchar *event)
{
   spldat      *sd = EFgamma.curves;
   int         ii, jj, nn, Fupdate = 0;
   spldat      sdtemp;

   if (zd->zstat)                                                          //  dialog complete
   {
      if (zd->zstat == 1) edit_done(EFgamma);
      else edit_cancel(EFgamma);
      return 1;
   }
   
   edit_takeover(EFgamma);                                                 //  set my edit function
   
   if (strEqu(event,"blendwidth")) signal_thread();                        //  adjust area edge blending

   if (strEqu(event,"hist")) m_histogram(0,0);                             //  show brightness distribution

   if (strstr("all red green blue",event)) {                               //  new choice of curve
      zdialog_fetch(zd,event,ii);
      if (! ii) return 0;                                                  //  ignore button OFF events

      ii = strcmpv(event,"all","red","green","blue",null);
      gamma_spc = ii;                                                      //  1, 2, 3, 4

      sd->nap[0] = sd->nap[ii];                                            //  copy active curve ii to curve 0
      for (jj = 0; jj < sd->nap[0]; jj++) {
         sd->apx[0][jj] = sd->apx[ii][jj];
         sd->apy[0][jj] = sd->apy[ii][jj];
      }

      Fupdate++;
   }

   if (strEqu(event,"load"))                                               //  load 4 saved curves from file
   {
      sdtemp.Nspc = 4;
      sdtemp.drawarea = 0;
      nn = splcurve_load(&sdtemp);
      if (nn != 1) return 0;

      for (ii = 0; ii < 4; ii++) {                                         //  load curves 0-3 to curves 1-4
         sd->vert[ii+1] = sdtemp.vert[ii];
         sd->nap[ii+1] = sdtemp.nap[ii];
         for (jj = 0; jj < sdtemp.nap[ii]; jj++) {
            sd->apx[ii+1][jj] = sdtemp.apx[ii][jj];
            sd->apy[ii+1][jj] = sdtemp.apy[ii][jj];
         }
         splcurve_generate(sd,ii+1);
      }

      ii = gamma_spc;                                                      //  current active curve, 1-4

      sd->nap[0] = sd->nap[ii];                                            //  copy active curve to curve 0
      for (jj = 0; jj < sd->nap[0]; jj++) {
         sd->apx[0][jj] = sd->apx[ii][jj];
         sd->apy[0][jj] = sd->apy[ii][jj];
      }

      Fupdate++;
   }

   if (strEqu(event,"save"))                                               //  save 4 curves to a file
   {
      sdtemp.Nspc = 4;
      for (ii = 0; ii < 4; ii++) {
         sdtemp.vert[ii] = sd->vert[ii+1];
         sdtemp.nap[ii] = sd->nap[ii+1];
         for (jj = 0; jj < sd->nap[ii+1]; jj++) {
            sdtemp.apx[ii][jj] = sd->apx[ii+1][jj];
            sdtemp.apy[ii][jj] = sd->apy[ii+1][jj];
         }
      }

      splcurve_save(&sdtemp);
   }

   if (Fupdate)                                                            //  curve has changed
   {
      splcurve_generate(sd,0);                                             //  regenerate curve 0
      
      ii = gamma_spc;                                                      //  active curve 1-4

      sd->nap[ii] = sd->nap[0];                                            //  copy curve 0 to active curve
      for (jj = 0; jj < sd->nap[0]; jj++) {
         sd->apx[ii][jj] = sd->apx[0][jj];
         sd->apy[ii][jj] = sd->apy[0][jj];
      }

      for (jj = 0; jj < 1000; jj++) 
         sd->yval[ii][jj] = sd->yval[0][jj];

      splcurve_draw(0,0,sd);                                               //  draw curve

      signal_thread();                                                     //  trigger image update
   }

   return 1;
}


//  this function is called when a curve is edited

void gamma_curvedit(int spc)
{
   int      ii = gamma_spc, jj;
   spldat   *sd = EFgamma.curves;                                          //  active curve, 1-4

   edit_takeover(EFgamma);                                                 //  set my edit function
   
   sd->nap[ii] = sd->nap[0];                                               //  copy curve 0 to active curve
   for (jj = 0; jj < sd->nap[0]; jj++) {
      sd->apx[ii][jj] = sd->apx[0][jj];
      sd->apy[ii][jj] = sd->apy[0][jj];
   }

   for (jj = 0; jj < 1000; jj++) 
      sd->yval[ii][jj] = sd->yval[0][jj];

   if (gamma_spc == 1) {                                                   //  "all" curve (1) was edited
      for (ii = 2; ii < 5; ii++) {                                         //  fix R/G/B curves (2-4) to match
         sd->nap[ii] = sd->nap[1];
         for (jj = 0; jj < sd->nap[1]; jj++) {
            sd->apx[ii][jj] = sd->apx[1][jj];
            sd->apy[ii][jj] = sd->apy[1][jj];
         }

         for (jj = 0; jj < 1000; jj++)
            sd->yval[ii][jj] = sd->yval[1][jj];
      }
   }

   signal_thread();
   return;
}


//  gamma thread function

void * gamma_thread(void *arg)
{
   void * gamma_wthread(void *);
   
   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request

      for (int ii = 0; ii < Nwt; ii++)                                     //  start worker threads
         start_wthread(gamma_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion
      
      mwpaint2();                                                          //  update window
      CEF->Fmod = 1;                                                       //  image3 modified
   }

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


void * gamma_wthread(void *arg)                                            //  worker thread function
{
   int         index = *((int *) arg);
   int         ii, dist = 0, px3, py3;
   uint16      *pix1, *pix3;
   double      red1, green1, blue1, maxrgb;
   double      red3, green3, blue3;
   double      coeff = 1000.0 / 65536.0;
   double      dold, dnew;
   spldat      *sd = EFgamma.curves;

   for (py3 = index; py3 < E3hh; py3 += Nwt)                               //  loop output pixels
   for (px3 = 0; px3 < E3ww; px3++)
   {
      if (Factivearea) {                                                   //  select area active
         ii = py3 * E3ww + px3;
         dist = sa_pixmap[ii];                                             //  distance from edge
         if (! dist) continue;                                             //  pixel outside area
      }
      
      pix1 = PXMpix(E1pxm16,px3,py3);                                      //  input pixel
      pix3 = PXMpix(E3pxm16,px3,py3);                                      //  output pixel
      
      red1 = pix1[0];                                                      //  input RGB brightness, 0-65535
      green1 = pix1[1];
      blue1 = pix1[2];
      
      ii = coeff * red1;                                                   //  range 0-999 for gamma curve index
      red3 = 65535.0 * sd->yval[2][ii];                                    //  output RGB brightness, 0-65535
      ii = coeff * green1;
      green3 = 65535.0 * sd->yval[3][ii];
      ii = coeff * blue1;
      blue3 = 65535.0 * sd->yval[4][ii];
      
      maxrgb = red3;
      if (green3 > maxrgb) maxrgb = green3;
      if (blue3 > maxrgb) maxrgb = blue3;
      
      if (maxrgb > 65535) {                                                //  stop overflow
         red3 = red3 * 65535 / maxrgb;
         green3 = green3 * 65535 / maxrgb;
         blue3 = blue3 * 65535 / maxrgb;
      }

      if (Factivearea && dist < sa_blend) {                                //  blend changes over blendwidth
         dnew = 1.0 * dist / sa_blend;
         dold = 1.0 - dnew;
         red3 = dnew * red3 + dold * red1;
         green3 = dnew * green3 + dold * green1;
         blue3 = dnew * blue3 + dold * blue1;
      }

      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;
   }

   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


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

//  Clip pixels outside the given low/high range and expand the remaining
//  brightness range back to the full range of 0 to 65535.

double      xbrangeD, xbrangeB;
editfunc    EFxbrange;

void m_xbrange(GtkWidget *, cchar *)                                       //  new v.10.1
{
   int      xbrange_dialog_event(zdialog *zd, cchar *event);
   void *   xbrange_thread(void *);
   
   cchar  *title = ZTX("Expand Brightness Range");
   cchar  *labD = ZTX("dark pixels");
   cchar  *labB = ZTX("bright pixels");

   zfuncs::F1_help_topic = "expand_brightness";                            //  v.10.8
   
   EFxbrange.funcname = "bright-range";
   EFxbrange.Fprev = 1;                                                    //  use preview
   EFxbrange.Farea = 2;                                                    //  select area usable
   EFxbrange.Fpara = 1;                                                    //  parallel edit OK
   EFxbrange.threadfunc = xbrange_thread;                                  //  thread function
   if (! edit_setup(EFxbrange)) return;                                    //  setup edit

   zdialog *zd = zdialog_new(title,mWin,Bdone,Bcancel,null);               //  revised v.11.01
   EFxbrange.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"expand");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|expand");
   zdialog_add_widget(zd,"vbox","vb3","hb1",0,"homog|space=8");
   zdialog_add_widget(zd,"label","labD","vb1",labD);
   zdialog_add_widget(zd,"label","labB","vb1",labB);
   zdialog_add_widget(zd,"hscale","clipD","vb2","0|99|0.5|0");
   zdialog_add_widget(zd,"hscale","clipB","vb2","0|99|0.5|0");
   zdialog_add_widget(zd,"label","darkval","vb3");
   zdialog_add_widget(zd,"label","briteval","vb3");

   zdialog_resize(zd,300,0);
   zdialog_help(zd,"expand_brightness");                                   //  zdialog help topic        v.11.08
   zdialog_run(zd,xbrange_dialog_event,"save");                            //  run dialog - parallel     v.11.07
   
   m_histogram(0,0);                                                       //  show brightness distribution    v.11.02

   xbrangeD = xbrangeB = 0;                                                //  initial clip = 0
   return;
}


//  dialog event and completion callback function

int xbrange_dialog_event(zdialog *zd, cchar *event)
{
   char     text[8];

   if (zd->zstat)                                                          //  dialog complete
   {
      if (zd->zstat == 1) edit_done(EFxbrange);
      else edit_cancel(EFxbrange);
      histogram_destroy();                                                 //  v.11.02
      return 1;
   }

   edit_takeover(EFxbrange);                                               //  set my edit function
   
   if (strEqu(event,"reset")) {                                            //  reset dialog controls      v.11.08
      zdialog_stuff(zd,"clipD",0);
      zdialog_stuff(zd,"clipB",0);
   }
   
   if (strEqu(event,"blendwidth")) signal_thread();                        //  v.10.3

   if (strEqu(event,"clipD")) {
      zdialog_fetch(zd,"clipD",xbrangeD);
      signal_thread();
   }

   if (strEqu(event,"clipB")) {
      zdialog_fetch(zd,"clipB",xbrangeB);
      signal_thread();
   }
   
   sprintf(text,"%.0f",xbrangeD);                                          //  numeric feedback       v.11.07
   zdialog_stuff(zd,"darkval",text);
   sprintf(text,"%.0f",xbrangeB);
   zdialog_stuff(zd,"briteval",text);

   return 0;
}


//  thread function

void * xbrange_thread(void *)
{
   int         ii, px, py, dist = 0;
   double      dark, bright, b1, b3, bf, f1, f2;
   double      red1, green1, blue1, red3, green3, blue3;
   uint16      *pix1, *pix3;
   
   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request

      dark = 0.01 * xbrangeD * 65536;                                      //  clipping brightness levels
      bright = (1.0 - 0.01 * xbrangeB) * 65536;

      for (py = 0; py < E3hh; py++)                                        //  loop all image pixels
      for (px = 0; px < E3ww; px++)
      {
         ii = py * E3ww + px;

         if (Factivearea) {                                                //  select area active     bugfix v.9.9
            dist = sa_pixmap[ii];                                          //  distance from edge
            if (! dist) continue;                                          //  pixel is outside area
         }

         pix1 = PXMpix(E1pxm16,px,py);                                     //  input pixel
         pix3 = PXMpix(E3pxm16,px,py);                                     //  output pixel

         b1 = pixbright(pix1);

         if (b1 < dark)                                                    //  clip dark pixels
            b3 = 0;
         else if (b1 > bright)                                             //  clip bright pixels
            b3 = bright;
         else b3 = b1;
         
         if (b3 > dark) b3 = b3 - dark;                                    //  expand the rest
         b3 = b3 * (65535.0 / (bright - dark));

         bf = b3 / (b1 + 1);                                               //  brightness ratio
         
         red1 = pix1[0];                                                   //  input RGB           v.11.08
         green1 = pix1[1];
         blue1 = pix1[2];
         
         red3 = bf * red1;                                                 //  output RGB
         green3 = bf * green1;
         blue3 = bf * blue1;
         
         if (red3 > 65535) red3 = 65535;
         if (green3 > 65535) green3 = 65535;
         if (blue3 > 65535) blue3 = 65535;
         
         if (Factivearea && dist < sa_blend) {                             //  select area is active,
            f1 = 1.0 * dist / sa_blend;                                    //    blend changes over sa_blend
            f2 = 1.0 - f1;
            red3 = f1 * red3 + f2 * red1;
            green3 = f1 * green3 + f2 * green1;
            blue3 = f1 * blue3 + f2 * blue1;
         }
         
         pix3[0] = red3;
         pix3[1] = green3;
         pix3[2] = blue3;
      }

      CEF->Fmod = 1;
      mwpaint2();
   }

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


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

//  flatten brightness distribution

double   flatten_value = 0;                                                //  flatten value, 0 - 100%
double   flatten_brdist[65536];

void  flatten_update_image();
void  flatten_distribution();                                              //  compute brightness distribution
void  flatten_pixel(int px, int py);                                       //  compute new pixel brightness

editfunc    EFflatten;

void m_flatten(GtkWidget *, cchar *)
{
   int    flatten_dialog_event(zdialog* zd, cchar *event);
   void * flatten_thread(void *);

   cchar  *title = ZTX("Flatten Brightness Distribution");
   zfuncs::F1_help_topic = "flatten";

   EFflatten.funcname = "flatten";
   EFflatten.Fprev = 1;                                                    //  preview
   EFflatten.Farea = 2;                                                    //  select area usable
   EFflatten.Fpara = 1;                                                    //  parallel edit OK
   EFflatten.threadfunc = flatten_thread;
   if (! edit_setup(EFflatten)) return;                                    //  setup edit

   zdialog *zd = zdialog_new(title,mWin,Bdone,Bcancel,null);
   EFflatten.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=15");
   zdialog_add_widget(zd,"label","labfd","hb1",ZTX("Flatten"),"space=5");
   zdialog_add_widget(zd,"hscale","flatten","hb1","0|100|1|0","expand");
   zdialog_add_widget(zd,"label","value","hb1",0,"space=5");

   zdialog_help(zd,"flatten");                                             //  v.11.08
   zdialog_resize(zd,300,0);
   zdialog_run(zd,flatten_dialog_event,"save");                            //  run dialog - parallel     v.11.07
   
   flatten_value = 0;
   return;
}


//  dialog event and completion callback function

int flatten_dialog_event(zdialog *zd, cchar *event)                        //  flatten dialog event function
{
   char     text[8];

   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(EFflatten);                            //  done
      else edit_cancel(EFflatten);                                         //  cancel or destroy
      return 0;
   }
   
   edit_takeover(EFflatten);                                               //  set my edit function
   
   if (strEqu(event,"blendwidth")) flatten_update_image();                 //  v.11.06

   if (strEqu(event,"flatten")) {
      zdialog_fetch(zd,"flatten",flatten_value);                           //  get slider value
      sprintf(text,"%.0f",flatten_value);                                  //  numeric feedback          v.11.07
      zdialog_stuff(zd,"value",text);
      flatten_update_image();                                              //  trigger update thread
   }
   
   return 1;
}


//  Update image based on select area and dialog controls

void flatten_update_image()                                                //  v.11.06
{
   int      px, py, ww, hh;

   if (Factivearea && sa_Npixel < 200000)                                  //  if relatively small area,
   {                                                                       //    process in main() thread   v.11.06
      flatten_distribution();

      for (py = sa_miny; py < sa_maxy; py++)
      for (px = sa_minx; px < sa_maxx; px++)
         flatten_pixel(px,py);

      px = sa_minx;                                                        //  direct window update
      py = sa_miny;
      ww = sa_maxx - sa_minx;
      hh = sa_maxy - sa_miny;
      mwpaint3(px,py,ww,hh);                                               //  speedup    v.11.06
      CEF->Fmod = 1;
   }
   
   else signal_thread();                                                   //  use thread process
   return;
}


//  thread function - use multiple working threads

void * flatten_thread(void *)
{
   void  * flatten_wthread(void *arg);

   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request
      
      flatten_distribution();
      
      for (int ii = 0; ii < Nwt; ii++)                                     //  start worker threads
         start_wthread(flatten_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion

      CEF->Fmod = 1;
      mwpaint2();                                                          //  update window
   }

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


void * flatten_wthread(void *arg)                                          //  worker thread function
{
   int         index = *((int *) (arg));
   int         px, py;

   for (py = index; py < E1hh; py += Nwt)                                  //  flatten brightness distribution
   for (px = 0; px < E1ww; px++)
      flatten_pixel(px,py);

   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


//  compute brightness distribution for image or selected area

void flatten_distribution()
{
   int         px, py, ii, npix;
   int         ww = E1ww, hh = E1hh;
   double      bright1;
   uint16      *pix1;

   for (ii = 0; ii < 65536; ii++)                                          //  clear brightness distribution data
      flatten_brdist[ii] = 0;

   if (Factivearea)                                                        //  process selected area
   {
      for (ii = npix = 0; ii < ww * hh; ii++)
      {
         if (! sa_pixmap[ii]) continue;
         py = ii / ww;
         px = ii - py * ww;
         pix1 = PXMpix(E1pxm16,px,py);
         bright1 = pixbright(pix1);
         flatten_brdist[int(bright1)]++;
         npix++;
      }
      
      for (ii = 1; ii < 65536; ii++)                                       //  cumulative brightness distribution
         flatten_brdist[ii] += flatten_brdist[ii-1];                       //   0 ... npix

      for (ii = 0; ii < 65536; ii++)
         flatten_brdist[ii] = flatten_brdist[ii]                           //  multiplier per brightness level
                            / npix * 65536.0 / (ii + 1);
   }

   else                                                                    //  process whole image
   {
      for (py = 0; py < hh; py++)                                          //  compute brightness distribution
      for (px = 0; px < ww; px++)
      {
         pix1 = PXMpix(E1pxm16,px,py);
         bright1 = pixbright(pix1);
         flatten_brdist[int(bright1)]++;
      }
      
      for (ii = 1; ii < 65536; ii++)                                       //  cumulative brightness distribution
         flatten_brdist[ii] += flatten_brdist[ii-1];                       //   0 ... (ww * hh)

      for (ii = 0; ii < 65536; ii++)
         flatten_brdist[ii] = flatten_brdist[ii]                           //  multiplier per brightness level 
                            / (ww * hh) * 65536.0 / (ii + 1);
   }
   
   return;
}


//  compute new brightness for one pixel

void flatten_pixel(int px, int py)
{
   int         ii, dist = 0;
   uint16      *pix1, *pix3;
   double      fold, fnew, dold, dnew, cmax;
   double      red1, green1, blue1, red3, green3, blue3;
   double      bright1, bright2;

   if (Factivearea) {                                                      //  select area active
      ii = py * Fww + px;
      dist = sa_pixmap[ii];                                                //  distance from edge
      if (! dist) return;                                                  //  outside pixel
   }

   pix1 = PXMpix(E1pxm16,px,py);                                           //  input pixel
   pix3 = PXMpix(E3pxm16,px,py);                                           //  output pixel
      
   fnew = 0.01 * flatten_value;                                            //  0.0 - 1.0  how much to flatten
   fold = 1.0 - fnew;                                                      //  1.0 - 0.0  how much to retain

   red1 = pix1[0];
   green1 = pix1[1];
   blue1 = pix1[2];

   bright1 = 0.25 * red1 + 0.65 * green1 + 0.10 * blue1;                   //  input brightness
   bright2 = flatten_brdist[int(bright1)];                                 //  output brightness adjustment

   red3 = bright2 * red1;                                                  //  flattened brightness
   green3 = bright2 * green1;
   blue3 = bright2 * blue1;

   red3 = fnew * red3 + fold * red1;                                       //  blend new and old brightness
   green3 = fnew * green3 + fold * green1;
   blue3 = fnew * blue3 + fold * blue1;

   if (Factivearea && dist < sa_blend) {                                   //  select area is active,
      dnew = 1.0 * dist / sa_blend;                                        //    blend changes over sa_blend
      dold = 1.0 - dnew;
      red3 = dnew * red3 + dold * red1;
      green3 = dnew * green3 + dold * green1;
      blue3 = dnew * blue3 + dold * blue1;
   }

   cmax = red3;                                                            //  stop overflow, keep color balance
   if (green3 > cmax) cmax = green3;
   if (blue3 > cmax) cmax = blue3;
   if (cmax > 65535) {
      cmax = 65535 / cmax;
      red3 = red3 * cmax;
      green3 = green3 * cmax;
      blue3 = blue3 * cmax;
   }

   pix3[0] = red3;
   pix3[1] = green3;
   pix3[2] = blue3;

   return;
}


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

//  ramp brightness across image, vertical and horizontal gradients

editfunc    EFbrramp;
int         brramp_spc;                                                    //  current spline curves (2 at a time)

void m_brightramp(GtkWidget *, cchar *)                                    //  upgrade for 4 colors      v.11.07
{
   int    brramp_dialog_event(zdialog* zd, cchar *event);
   void   brramp_curvedit(int spc);
   void * brramp_thread(void *);
   void   brramp_init();

   zfuncs::F1_help_topic = "brightness_ramp";

   EFbrramp.funcname = "bright-ramp";
   EFbrramp.Fprev = 1;                                                     //  use preview
   EFbrramp.Farea = 2;                                                     //  select area usable
   EFbrramp.Fpara = 1;                                                     //  parallel edit OK
   EFbrramp.threadfunc = brramp_thread;
   if (! edit_setup(EFbrramp)) return;                                     //  setup edit
   
/***
          _________________________________________
         |                                         |
         |                                         |
         |           curve edit area               |
         |                                         |
         |                                         |
         |_________________________________________|

            (o) all  (o) red  (o) green  (o) blue                          //  colors: all, red, green, blue
            Curve File: [Open]  [Save]                                     //  each has vert. and horiz. curve
                                   [Done]  [Cancel]    

***/

   zdialog *zd = zdialog_new(ZTX("Ramp brightness across image"),mWin,Bdone,Bcancel,null);
   EFbrramp.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5|expand");
   zdialog_add_widget(zd,"vbox","vb1","hb1");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"expand");
   zdialog_add_widget(zd,"label","lmax","vb1","+","space=3");
   zdialog_add_widget(zd,"label","lspace","vb1",0,"expand");
   zdialog_add_widget(zd,"label","lmin","vb1","‒","space=3");
   zdialog_add_widget(zd,"label","lspace","vb1");
   zdialog_add_widget(zd,"frame","frame","vb2",0,"expand");

   zdialog_add_widget(zd,"hbox","hb2","vb2");
   zdialog_add_widget(zd,"label","lmin","hb2","‒","space=3");
   zdialog_add_widget(zd,"label","lspace","hb2",0,"expand");
   zdialog_add_widget(zd,"label","lmax","hb2","+","space=3");
   zdialog_add_widget(zd,"hbox","hbrgb","dialog",0,"space=3");
   zdialog_add_widget(zd,"radio","all","hbrgb",Ball,"space=5");
   zdialog_add_widget(zd,"radio","red","hbrgb",Bred,"space=5");
   zdialog_add_widget(zd,"radio","green","hbrgb",Bgreen,"space=5");
   zdialog_add_widget(zd,"radio","blue","hbrgb",Bblue,"space=5");
   zdialog_add_widget(zd,"hbox","hbcf","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labcf","hbcf",Bcurvefile,"space=8");
   zdialog_add_widget(zd,"button","load","hbcf",Bopen,"space=5");
   zdialog_add_widget(zd,"button","save","hbcf",Bsave,"space=5");

   GtkWidget *frame = zdialog_widget(zd,"frame");
   spldat *sd = splcurve_init(frame,brramp_curvedit);                      //  v.11.01
   EFbrramp.curves = sd;
   
   brramp_init();                                                          //  set up initial flat curves

   sd->Nspc = 2;                                                           //  only curves 0-1 are shown
   brramp_spc = 2;                                                         //  current curves: all (2-3)
   
   zdialog_stuff(zd,"all",1);                                              //  stuff default selection, all

   zdialog_resize(zd,260,350);
   zdialog_help(zd,"brightness_ramp");                                     //  zdialog help topic        v.11.08
   zdialog_run(zd,brramp_dialog_event,"save");                             //  run dialog - parallel     v.11.07
   return;
}


//  initialize all curves to flat

void brramp_init()
{
   spldat *sd = EFbrramp.curves;

   for (int ii = 0; ii < 10; ii++)                                         //  loop curves: current (0-1), all (2-3),
   {                                                                       //    red (4-5), green (6-7), blue (8-9)
      sd->nap[ii] = 2;                                                     //  horizontal curve ii
      sd->vert[ii] = 0;
      sd->apx[ii][0] = 0.01;
      sd->apx[ii][1] = 0.99;
      sd->apy[ii][0] = sd->apy[ii][1] = 0.5;
      splcurve_generate(sd,ii);

      ii++;
      sd->nap[ii] = 2;                                                     //  vertical curve ii+1
      sd->vert[ii] = 1;
      sd->apx[ii][0] = 0.01;
      sd->apx[ii][1] = 0.99;
      sd->apy[ii][0] = sd->apy[ii][1] = 0.5;
      splcurve_generate(sd,ii);
   }
   
   return;
}


//  dialog event and completion callback function

int brramp_dialog_event(zdialog *zd, cchar *event)
{
   spldat      *sd = EFbrramp.curves;
   int         ii, jj, nn, Fupdate = 0;
   spldat      sdtemp;

   if (zd->zstat)                                                          //  dialog complete
   {
      if (zd->zstat == 1) edit_done(EFbrramp);
      else edit_cancel(EFbrramp);
      return 1;
   }
   
   edit_takeover(EFbrramp);                                                //  set my edit function
   
   if (strEqu(event,"reset")) {                                            //  reset dialog controls     v.11.08
      brramp_init();
      Fupdate++;
   }

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

   if (strstr("all red green blue",event)) {                               //  new choice of curve
      zdialog_fetch(zd,event,ii);
      if (! ii) return 0;                                                  //  ignore button OFF events

      ii = strcmpv(event,"all","red","green","blue",null);
      brramp_spc = ii = 2 * ii;                                            //  2, 4, 6, 8

      sd->nap[0] = sd->nap[ii];                                            //  copy active curves ii/ii+1 to curves 0/1
      sd->nap[1] = sd->nap[ii+1];
      for (jj = 0; jj < sd->nap[0]; jj++) {
         sd->apx[0][jj] = sd->apx[ii][jj];
         sd->apy[0][jj] = sd->apy[ii][jj];
      }
      for (jj = 0; jj < sd->nap[1]; jj++) {
         sd->apx[1][jj] = sd->apx[ii+1][jj];
         sd->apy[1][jj] = sd->apy[ii+1][jj];
      }

      Fupdate++;
   }

   if (strEqu(event,"load"))                                               //  load 8 saved curves from file
   {
      sdtemp.Nspc = 8;
      sdtemp.drawarea = 0;
      nn = splcurve_load(&sdtemp);
      if (nn != 1) return 0;

      for (ii = 0; ii < 8; ii++) {                                         //  load curves 0-7 to curves 2-9
         sd->vert[ii+2] = sdtemp.vert[ii];
         sd->nap[ii+2] = sdtemp.nap[ii];
         for (jj = 0; jj < sdtemp.nap[ii]; jj++) {
            sd->apx[ii+2][jj] = sdtemp.apx[ii][jj];
            sd->apy[ii+2][jj] = sdtemp.apy[ii][jj];
         }
         splcurve_generate(sd,ii+2);
      }

      ii = brramp_spc;                                                     //  current active curves: 2, 4, 6, 8

      sd->nap[0] = sd->nap[ii];                                            //  copy 2 active curves to curves 0-1
      for (jj = 0; jj < sd->nap[0]; jj++) {
         sd->apx[0][jj] = sd->apx[ii][jj];
         sd->apy[0][jj] = sd->apy[ii][jj];
      }

      sd->nap[1] = sd->nap[ii+1];
      for (jj = 0; jj < sd->nap[1]; jj++) {
         sd->apx[1][jj] = sd->apx[ii+1][jj];
         sd->apy[1][jj] = sd->apy[ii+1][jj];
      }

      Fupdate++;
   }

   if (strEqu(event,"save"))                                               //  save 8 curves to file
   {
      sdtemp.Nspc = 8;
      for (ii = 0; ii < 8; ii++) {
         sdtemp.vert[ii] = sd->vert[ii+2];
         sdtemp.nap[ii] = sd->nap[ii+2];
         for (jj = 0; jj < sd->nap[ii+2]; jj++) {
            sdtemp.apx[ii][jj] = sd->apx[ii+2][jj];
            sdtemp.apy[ii][jj] = sd->apy[ii+2][jj];
         }
      }

      splcurve_save(&sdtemp);
   }

   if (Fupdate)                                                            //  curve has changed
   {
      splcurve_generate(sd,0);                                             //  regenerate curves 0-1
      splcurve_generate(sd,1);
      
      ii = brramp_spc;                                                     //  active curves

      sd->nap[ii] = sd->nap[0];                                            //  copy curves 0-1 to active curves
      for (jj = 0; jj < sd->nap[0]; jj++) {
         sd->apx[ii][jj] = sd->apx[0][jj];
         sd->apy[ii][jj] = sd->apy[0][jj];
      }

      sd->nap[ii+1] = sd->nap[1];
      for (jj = 0; jj < sd->nap[1]; jj++) {
         sd->apx[ii+1][jj] = sd->apx[1][jj];
         sd->apy[ii+1][jj] = sd->apy[1][jj];
      }

      for (jj = 0; jj < 1000; jj++) 
      {
         sd->yval[ii][jj] = sd->yval[0][jj];
         sd->yval[ii+1][jj] = sd->yval[1][jj];
      }

      splcurve_draw(0,0,sd);                                               //  draw curves

      signal_thread();                                                     //  trigger image update
   }

   return 1;
}


//  this function is called when a curve is edited

void brramp_curvedit(int spc)
{
   int      ii = brramp_spc, jj;
   spldat   *sd = EFbrramp.curves;                                         //  active curves: 2, 4, 6, 8 (and +1)

   edit_takeover(EFbrramp);                                                //  set my edit function
   
   sd->nap[ii] = sd->nap[0];                                               //  copy curves 0-1 to active curves
   for (jj = 0; jj < sd->nap[0]; jj++) {
      sd->apx[ii][jj] = sd->apx[0][jj];
      sd->apy[ii][jj] = sd->apy[0][jj];
   }

   sd->nap[ii+1] = sd->nap[1];
   for (jj = 0; jj < sd->nap[1]; jj++) {
      sd->apx[ii+1][jj] = sd->apx[1][jj];
      sd->apy[ii+1][jj] = sd->apy[1][jj];
   }

   for (jj = 0; jj < 1000; jj++) {
      sd->yval[ii][jj] = sd->yval[0][jj];
      sd->yval[ii+1][jj] = sd->yval[1][jj];
   }

   if (brramp_spc == 2) {                                                  //  "all" curve was edited
      for (ii = 4; ii < 10; ii += 2) {                                     //  fix R/G/B curves to match
         sd->nap[ii] = sd->nap[2];
         for (jj = 0; jj < sd->nap[2]; jj++) {
            sd->apx[ii][jj] = sd->apx[2][jj];
            sd->apy[ii][jj] = sd->apy[2][jj];
         }
         for (jj = 0; jj < 1000; jj++)
            sd->yval[ii][jj] = sd->yval[2][jj];
      }
      for (ii = 5; ii < 10; ii += 2) {
         sd->nap[ii] = sd->nap[3];
         for (jj = 0; jj < sd->nap[3]; jj++) {
            sd->apx[ii][jj] = sd->apx[3][jj];
            sd->apy[ii][jj] = sd->apy[3][jj];
         }
         for (jj = 0; jj < 1000; jj++)
            sd->yval[ii][jj] = sd->yval[3][jj];
      }
   }

   signal_thread();
   return;
}


//  brramp thread function

void * brramp_thread(void *arg)
{
   void * brramp_wthread(void *);
   
   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request

      for (int ii = 0; ii < Nwt; ii++)                                     //  start worker threads
         start_wthread(brramp_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion
      
      mwpaint2();                                                          //  update window
      CEF->Fmod = 1;                                                       //  image3 modified
   }

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


void * brramp_wthread(void *arg)                                           //  worker thread function
{
   int         index = *((int *) arg);
   int         ii, jj, kk, dist = 0, px3, py3;
   uint16      *pix1, *pix3;
   double      red1, green1, blue1, maxrgb;
   double      red3, green3, blue3;
   double      dispx, dispy;
   double      hramp[3], vramp[3], tramp[3];
   double      spanw, spanh, dold, dnew;
   spldat      *sd = EFbrramp.curves;

   if (Factivearea) {                                                      //  if select area active, ramp
      spanw = sa_maxx - sa_minx;                                           //    brightness over enclosing rectangle
      spanh = sa_maxy - sa_miny;
   }
   else {
      spanw = E3ww;                                                        //  else over entire image
      spanh = E3hh;
   }   

   for (py3 = index; py3 < E3hh; py3 += Nwt)                               //  loop output pixels
   for (px3 = 0; px3 < E3ww; px3++)
   {
      if (Factivearea) {                                                   //  select area active        v.10.1
         ii = py3 * E3ww + px3;
         dist = sa_pixmap[ii];                                             //  distance from edge
         if (! dist) continue;                                             //  pixel outside area
         dispx = (px3 - sa_minx) / spanw;
         dispy = (py3 - sa_miny) / spanh;
      }
      else {
         dispx = px3 / spanw;                                              //  left > right = 0 to 1
         dispy = py3 / spanh;                                              //  top > bottom = 0 to 1
      }
      
      kk = 1000 * dispx;                                                   //  conv. dispx 0-1 to 0-1000
      if (kk > 999) kk = 999;

      for (ii = 0, jj = 4; ii < 3; ii++, jj+=2)                            //  horz. curves for dispx and R/G/B
         hramp[ii] = sd->yval[jj][kk] - 0.5;                               //  scale to -0.5 to +0.5
      
      kk = 1000 * dispy;                                                   //  conv. dispy
      if (kk > 999) kk = 999;

      for (ii = 0, jj = 5; ii < 3; ii++, jj+=2)                            //  vert. curves
         vramp[ii] = sd->yval[jj][kk] - 0.5;

      for (ii = 0; ii < 3; ii++)                                           //  total R/G/B change
         tramp[ii] = 1.0 + hramp[ii] + vramp[ii];                          //  scale to 0.0 to 2.0

      pix1 = PXMpix(E1pxm16,px3,py3);                                      //  input pixel
      red1 = pix1[0];
      green1 = pix1[1];
      blue1 = pix1[2];
      
      red3 = red1 * tramp[0];                                              //  change R/G/B
      green3 = green1 * tramp[1];
      blue3 = blue1 * tramp[2];

      maxrgb = red3;
      if (green3 > maxrgb) maxrgb = green3;
      if (blue3 > maxrgb) maxrgb = blue3;
      
      if (maxrgb > 65535) {                                                //  stop overflow
         red3 = red3 * 65535 / maxrgb;
         green3 = green3 * 65535 / maxrgb;
         blue3 = blue3 * 65535 / maxrgb;
      }

      if (Factivearea && dist < sa_blend) {                                //  blend changes over blendwidth   v.10.1
         dnew = 1.0 * dist / sa_blend;
         dold = 1.0 - dnew;
         red3 = dnew * red3 + dold * red1;
         green3 = dnew * green3 + dold * green1;
         blue3 = dnew * blue3 + dold * blue1;
      }

      pix3 = PXMpix(E3pxm16,px3,py3);                                      //  output pixel
      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;
   }

   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


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

   Image Tone Mapping function
   enhance local contrast as opposed to overall contrast

   methodology:
   get brightness gradients for each pixel in 4 directions: SE SW NE NW
   amplify gradients using the edit curve (x-axis range 0-max. gradient)
   integrate 4 new brightness surfaces from the amplified gradients:
     - pixel brightness = prior pixel brightness + amplified gradient
     - the Amplify control varies amplification from zero to overamplified
   new pixel brightness = average from 4 calculated brightness surfaces

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

float    *Tmap_brmap1;
float    *Tmap_brmap3[4];
int      Tmap_contrast99;
double   Tmap_amplify;

editfunc    EFtonemap;


void m_tonemap(GtkWidget *, cchar *)                                       //  new v.9.8
{
   int      Tmap_dialog_event(zdialog *zd, cchar *event);
   void     Tmap_curvedit(int);
   void *   Tmap_thread(void *);

   zfuncs::F1_help_topic = "tone_mapping";                                 //  v.10.8

   int         ii, cc, px, py;
   uint16      *pix1;
   cchar       *title = ZTX("Tone Mapping");
   int         jj, sum, limit, condist[100];

   EFtonemap.funcname = "tonemap";
   EFtonemap.Farea = 2;                                                    //  select area usable
   EFtonemap.Fpara = 1;                                                    //  parallel edit OK
   EFtonemap.threadfunc = Tmap_thread;
   if (! edit_setup(EFtonemap)) return;                                    //  setup: no preview, select area OK

/***
            _____________________________
           |                             |
           |                             |
           |    curve drawing area       |
           |                             |
           |_____________________________| 
            low       contrast       high

            Amplify ========[]=========

            Curve File: [ Open ] [ Save ]
***/

   zdialog *zd = zdialog_new(title,mWin,Bdone,Bcancel,null);
   EFtonemap.zd = zd;

   zdialog_add_widget(zd,"frame","frame","dialog",0,"expand");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labcL","hb1",ZTX("low"),"space=4");
   zdialog_add_widget(zd,"label","labcM","hb1",ZTX("contrast"),"expand");
   zdialog_add_widget(zd,"label","labcH","hb1",ZTX("high"),"space=5");

   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labcon","hb2",ZTX("Amplify"),"space=5");
   zdialog_add_widget(zd,"hscale","amplify","hb2","0|1|0.01|0","expand");
   zdialog_add_widget(zd,"label","ampval","hb2",0,"space=5");

   zdialog_add_widget(zd,"hbox","hbcf","dialog",0,"space=8");
   zdialog_add_widget(zd,"label","labcf","hbcf",Bcurvefile,"space=5");
   zdialog_add_widget(zd,"button","load","hbcf",Bopen,"space=5");
   zdialog_add_widget(zd,"button","save","hbcf",Bsave,"space=5");

   GtkWidget *frame = zdialog_widget(zd,"frame");                          //  set up curve edit
   spldat *sd = splcurve_init(frame,Tmap_curvedit);                        //  v.11.01
   EFtonemap.curves = sd;

   sd->Nspc = 1;
   sd->vert[0] = 0;
   sd->nap[0] = 3;                                                         //  initial curve anchor points
   sd->apx[0][0] = 0.01;
   sd->apy[0][0] = 0.2;
   sd->apx[0][1] = 0.50;
   sd->apy[0][1] = 0.4;
   sd->apx[0][2] = 0.99;
   sd->apy[0][2] = 0.0;

   splcurve_generate(sd,0);                                                //  generate curve data

   cc = Fww * Fhh * sizeof(float);                                         //  allocate brightness map memory
   Tmap_brmap1 = (float *) zmalloc(cc,"tonemap");
   for (ii = 0; ii < 4; ii++)
      Tmap_brmap3[ii] = (float *) zmalloc(cc,"tonemap");

   for (py = 0; py < Fhh; py++)                                            //  map initial image brightness
   for (px = 0; px < Fww; px++)
   {
      ii = py * Fww + px;
      pix1 = PXMpix(E1pxm16,px,py);
      Tmap_brmap1[ii] = pixbright(pix1);
   }

   for (ii = 0; ii < 100; ii++) 
      condist[ii] = 0;

   for (py = 1; py < Fhh; py++)                                            //  map contrast distribution
   for (px = 1; px < Fww; px++)
   {
      ii = py * Fww + px;
      jj = 0.00152 * fabsf(Tmap_brmap1[ii] - Tmap_brmap1[ii-1]);           //  contrast, ranged 0 - 99
      condist[jj]++;
      jj = 0.00152 * fabsf(Tmap_brmap1[ii] - Tmap_brmap1[ii-Fww]);
      condist[jj]++;
   }
   
   sum = 0;
   limit = 0.99 * 2 * (Fww-1) * (Fhh-1);                                   //  find 99th percentile contrast

   for (ii = 0; ii < 100; ii++) {
      sum += condist[ii];
      if (sum > limit) break;
   }
   
   Tmap_contrast99 = 65535.0 * ii / 100.0;                                 //  0 to 65535
   if (Tmap_contrast99 < 1000) Tmap_contrast99 = 1000;                     //  rescale low-contrast image   v.10.9

   zdialog_resize(zd,300,300);
   zdialog_help(zd,"tone_mapping");                                        //  zdialog help topic        v.11.08
   zdialog_run(zd,Tmap_dialog_event,"save");                               //  run dialog - parallel     v.11.07

   Tmap_amplify = 0;
   return;
}


//  dialog event and completion callback function

int Tmap_dialog_event(zdialog *zd, cchar *event)
{
   spldat   *sd = EFtonemap.curves;
   char     text[8];

   if (zd->zstat)                                                          //  dialog complete
   {
      if (zd->zstat == 1) edit_done(EFtonemap);
      else edit_cancel(EFtonemap);
      zfree(Tmap_brmap1);                                                  //  free memory
      for (int ii = 0; ii < 4; ii++)
      zfree(Tmap_brmap3[ii]);
      return 1;
   }

   edit_takeover(EFtonemap);                                               //  set my edit function

   if (strEqu(event,"reset"))                                              //  reset dialog controls      v.11.08
      zdialog_stuff(zd,"amplify",0);

   if (strEqu(event,"blendwidth")) signal_thread();                        //  v.10.3

   if (strEqu(event,"amplify")) {                                          //  slider value
      zdialog_fetch(zd,"amplify",Tmap_amplify);
      sprintf(text,"%.2f",Tmap_amplify);                                   //  numeric feedback       v.11.07
      zdialog_stuff(zd,"ampval",text);
      signal_thread();                                                     //  trigger update thread
   }

   if (strEqu(event,"load")) {                                             //  load saved curve       v.11.02
      splcurve_load(sd);
      signal_thread();
      return 0;
   }

   if (strEqu(event,"save")) {                                             //  save curve to file     v.11.02
      splcurve_save(sd);
      return 0;
   }
   
   return 0;
}


//  this function is called when the curve is edited

void Tmap_curvedit(int)
{
   edit_takeover(EFtonemap);                                               //  set my edit function
   signal_thread();
   return;
}


//  thread function

void * Tmap_thread(void *)
{
   void * Tmap_wthread1(void *arg);
   void * Tmap_wthread2(void *arg);

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

      for (ii = 0; ii < 4; ii++)                                           //  start working threads1 (must be 4)
         start_wthread(Tmap_wthread1,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion
      
      for (ii = 0; ii < Nwt; ii++)                                         //  start working threads2
         start_wthread(Tmap_wthread2,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion
      
      CEF->Fmod = 1;
      mwpaint2(); 
   }

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


//  working threads

void * Tmap_wthread1(void *arg)                                            //  hardened   v.9.9
{
   int         ii, kk, bii, pii, dist = 0;
   int         px, px1, px2, pxinc;
   int         py, py1, py2, pyinc;
   float       b1, b3, xval, yval, grad;
   float       amplify, contrast99;
   spldat      *sd = EFtonemap.curves;

   contrast99 = Tmap_contrast99;                                           //  99th percentile contrast
   contrast99 = 1.0 / contrast99;                                          //  inverted

   amplify = pow(Tmap_amplify,0.2);                                        //  get amplification, 0 to 1    v.11.02

   bii = *((int *) arg);

   if (bii == 0) {                                                         //  direction SE
      px1 = 1; px2 = E3ww; pxinc = 1;
      py1 = 1; py2 = E3hh; pyinc = 1;
      pii = - 1 - E3ww;
   }
   
   else if (bii == 1) {                                                    //  direction SW
      px1 = E3ww-2; px2 = 0; pxinc = -1;
      py1 = 1; py2 = E3hh; pyinc = 1;
      pii = + 1 - E3ww;
   }
   
   else if (bii == 2) {                                                    //  direction NE
      px1 = 1; px2 = E3ww; pxinc = 1;
      py1 = E3hh-2; py2 = 0; pyinc = -1;
      pii = - 1 + E3ww;
   }
   
   else {   /* bii == 3 */                                                 //  direction NW
      px1 = E3ww-2; px2 = 0; pxinc = -1;
      py1 = E3hh-2; py2 = 0; pyinc = -1;
      pii = + 1 + E3ww;
   }

   for (ii = 0; ii < E3ww * E3hh; ii++)                                    //  initial brightness map
      Tmap_brmap3[bii][ii] = Tmap_brmap1[ii];

   for (py = py1; py != py2; py += pyinc)                                  //  loop all image pixels
   for (px = px1; px != px2; px += pxinc)
   {
      ii = py * E3ww + px;

      if (Factivearea) {                                                   //  select area active
         dist = sa_pixmap[ii];                                             //  distance from edge
         if (! dist) continue;                                             //  outside pixel
      }

      b1 = Tmap_brmap1[ii];                                                //  this pixel brightness
      grad = b1 - Tmap_brmap1[ii+pii];                                     //   - prior pixel --> gradient

      xval = fabsf(grad) * contrast99;                                     //  gradient scaled 0 to 1+
      kk = 1000.0 * xval;
      if (kk > 999) kk = 999;                                              //  speedup       v.11.06
      yval = 1.0 + 5.0 * sd->yval[0][kk];

      grad = grad * yval;                                                  //  magnified gradient

      b3 = Tmap_brmap3[bii][ii+pii] + grad;                                //  pixel brightness = prior + gradient
      b3 = (1.0 - amplify) * b1 + amplify * b3;                            //  constrain: push b3 toward b1    v.11.02

      if (b3 > 65535) b3 = 65535;                                          //  constrain
      if (b3 < 10) b3 = 10;

      Tmap_brmap3[bii][ii] = b3;                                           //  new pixel brightness
   }
   
   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


void * Tmap_wthread2(void *arg)                                            //  speedup   v.10.2
{
   uint16      *pix1, *pix3;
   int         index, ii, px, py, dist = 0;
   float       b1, b3, bf, f1, f2;
   float       red1, green1, blue1, red3, green3, blue3;

   index = *((int *) arg);

   for (py = index; py < E3hh; py += Nwt)                                  //  loop all image pixels
   for (px = 0; px < E3ww; px++)
   {
      ii = py * E3ww + px;

      if (Factivearea) {                                                   //  select area active     bugfix v.9.9
         dist = sa_pixmap[ii];                                             //  distance from edge
         if (! dist) continue;                                             //  pixel is outside area
      }

      pix1 = PXMpix(E1pxm16,px,py);                                        //  input pixel
      pix3 = PXMpix(E3pxm16,px,py);                                        //  output pixel

      b1 = Tmap_brmap1[ii];                                                //  initial pixel brightness
      b3 = Tmap_brmap3[0][ii] + Tmap_brmap3[1][ii]                         //  new brightness = average of four
         + Tmap_brmap3[2][ii] + Tmap_brmap3[3][ii];                        //    calculated brightness surfaces

      bf = 0.25 * b3 / (b1 + 1);                                           //  brightness ratio
      
      red1 = pix1[0];                                                      //  input RGB
      green1 = pix1[1];
      blue1 = pix1[2];
      
      red3 = bf * red1;                                                    //  output RGB
      if (red3 > 65535) red3 = 65535;
      green3 = bf * green1;
      if (green3 > 65535) green3 = 65535;
      blue3 = bf * blue1;
      if (blue3 > 65535) blue3 = 65535;

      if (Factivearea && dist < sa_blend) {                                //  select area is active,
         f1 = 1.0 * dist / sa_blend;                                       //    blend changes over sa_blend
         f2 = 1.0 - f1;
         red3 = f1 * red3 + f2 * red1;
         green3 = f1 * green3 + f2 * green1;
         blue3 = f1 * blue3 + f2 * blue1;
      }
      
      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;
   }

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


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

//  adjust white balance

void   whitebal_mousefunc();

double      whitebal_red, whitebal_green, whitebal_blue;
editfunc    EFwhitebal;

void m_whitebal(GtkWidget *, cchar *)
{
   int    whitebal_dialog_event(zdialog *zd, cchar *event);
   void * whitebal_thread(void *);

   cchar  *wbtitle = ZTX("Adjust White Balance");
   cchar  *wbhelp = ZTX("Click white or gray image location");

   zfuncs::F1_help_topic = "white_balance";                                //  v.10.8
   
   EFwhitebal.funcname = "white-balance";
   EFwhitebal.Fprev = 1;                                                   //  use preview
   EFwhitebal.Farea = 2;                                                   //  select area usable
   EFwhitebal.Fpara = 1;                                                   //  parallel edits OK
   EFwhitebal.threadfunc = whitebal_thread;                                //  thread function
   EFwhitebal.mousefunc = whitebal_mousefunc;                              //  mouse function
   if (! edit_setup(EFwhitebal)) return;                                   //  setup edit: preview

   zdialog *zd = zdialog_new(wbtitle,mWin,Bdone,Bcancel,null);
   EFwhitebal.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10");
   zdialog_add_widget(zd,"label","labwbh","hb1",wbhelp,"space=5");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labpix","hb2","pixel:");
   zdialog_add_widget(zd,"label","pixel","hb2","0000 0000");
   zdialog_add_widget(zd,"label","labrgb","hb2","   RGB:");
   zdialog_add_widget(zd,"label","rgb","hb2","000 000 000");

   zdialog_help(zd,"white_balance");                                       //  zdialog help topic        v.11.08
   zdialog_run(zd,whitebal_dialog_event,"save");                           //  run dialog - parallel     v.11.07

   whitebal_red = whitebal_green = whitebal_blue = 1.0;

   takeMouse(zd,whitebal_mousefunc,dragcursor);                            //  connect mouse function    v.11.03
   return;
}


//  dialog event and completion callback function

int whitebal_dialog_event(zdialog *zd, cchar *event)                       //  dialog event function     v.10.2
{
   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(EFwhitebal);                           //  done
      else edit_cancel(EFwhitebal);                                        //  cancel or destroy
      return 0;
   }
   
   edit_takeover(EFwhitebal);                                              //  set my edit function
   
   if (strEqu(event,"focus"))                                              //  toggle mouse capture         v.12.01
      takeMouse(zd,whitebal_mousefunc,dragcursor);                         //  connect mouse function
   
   if (strEqu(event,"blendwidth")) signal_thread();                        //  v.10.3
   return 1;
}


void whitebal_mousefunc()                                                  //  mouse function
{
   int         px, py, dx, dy;
   double      red, green, blue, rgbmean;
   char        work[40];
   uint16      *ppix16;
   zdialog     *zd = EFwhitebal.zd;
   
   if (! LMclick) return;
   
   edit_takeover(EFwhitebal);                                              //  set my edit function

   LMclick = 0;
   px = Mxclick;                                                           //  mouse click position
   py = Myclick;
   
   if (px < 2) px = 2;                                                     //  pull back from edge
   if (px > E3ww-3) px = E3ww-3;
   if (py < 2) py = 2;
   if (py > E3hh-3) py = E3hh-3;
   
   red = green = blue = 0;

   for (dy = -2; dy <= 2; dy++)                                            //  5x5 block around mouse position
   for (dx = -2; dx <= 2; dx++)
   {
      ppix16 = PXMpix(E1pxm16,px+dx,py+dy);                                //  input image
      red += ppix16[0];
      green += ppix16[1];
      blue += ppix16[2];
   }

   red = red / 25.0;                                                       //  mean RGB levels 
   green = green / 25.0;
   blue = blue / 25.0;
   rgbmean = (red + green + blue) / 3.0;

   whitebal_red = rgbmean / red;
   whitebal_green = rgbmean / green;
   whitebal_blue = rgbmean / blue;

   signal_thread();                                                        //  trigger image update

   snprintf(work,40,"%d %d",px,py);
   zdialog_stuff(zd,"pixel",work);

   snprintf(work,40,"%7.3f %7.3f %7.3f",red/256,green/256,blue/256);
   zdialog_stuff(zd,"rgb",work);
   
   return;
}


//  Update image based on neutral pixel that was clicked

void * whitebal_thread(void *)
{
   void * whitebal_wthread(void *arg);

   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request
      
      for (int ii = 0; ii < Nwt; ii++)                                     //  start worker threads
         start_wthread(whitebal_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion

      CEF->Fmod = 1;
      mwpaint2();                                                          //  update window
   }

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


void * whitebal_wthread(void *arg)                                         //  worker thread function
{
   int         index = *((int *) arg);
   int         px, py, ii, dist = 0;
   uint16      *pix1, *pix3;
   double      red1, green1, blue1;
   double      red3, green3, blue3;
   double      brmax, dold, dnew;

   for (py = index; py < E1hh; py += Nwt)
   for (px = 0; px < E1ww; px++)
   {
      if (Factivearea) {                                                   //  select area active
         ii = py * Fww + px;
         dist = sa_pixmap[ii];                                             //  distance from edge
         if (! dist) continue;                                             //  pixel outside area
      }

      pix1 = PXMpix(E1pxm16,px,py);                                        //  input pixel
      pix3 = PXMpix(E3pxm16,px,py);                                        //  output pixel
      
      red1 = pix1[0];
      green1 = pix1[1];
      blue1 = pix1[2];
      
      red3 = whitebal_red * red1;                                          //  change color ratios
      green3 = whitebal_green * green1;
      blue3 = whitebal_blue * blue1;

      if (Factivearea && dist < sa_blend) {                                //  select area is active,
         dnew = 1.0 * dist / sa_blend;                                     //    blend changes over sa_blend
         dold = 1.0 - dnew;
         red3 = dnew * red3 + dold * red1;
         green3 = dnew * green3 + dold * green1;
         blue3 = dnew * blue3 + dold * blue1;
      }
      
      brmax = red3;                                                        //  brmax = brightest color
      if (green3 > brmax) brmax = green3;
      if (blue3 > brmax) brmax = blue3;
      
      if (brmax > 65535) {                                                 //  if overflow, reduce
         brmax = 65535 / brmax;
         red3 = red3 * brmax;
         green3 = green3 * brmax;
         blue3 = blue3 * brmax;
      }

      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;
   }
   
   exit_wthread();
   return 0;                                                               //  not executed, stop gcc warning
}


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

//  match_color edit function
//  Adjust colors of image 2 to match the colors of image 1
//  using small selected areas in each image as the match standard.

void * match_color_thread(void *);
void   match_color_mousefunc();

int      match_color_RGB1[3];                                              //  image 1 base colors to match
int      match_color_RGB2[3];                                              //  image 2 target colors to match
int      match_color_radius = 10;                                          //  mouse radius
int      match_color_mode = 0;

editfunc    EFmatchcolor;


void m_match_color(GtkWidget *, const char *)                              //  new v.11.07
{
   int    match_color_dialog_event(zdialog* zd, const char *event);
   
   cchar    *title = ZTX("Color Match Images");

   zfuncs::F1_help_topic = "match_colors";
   
   if (is_syncbusy()) return;                                              //  must wait for file sync      v.11.11
   if (! menulock(1)) return;                                              //  test menu lock
   menulock(0);
   
/*
               Color Match Images
         1  [ 10 ]   mouse radius for color sample
         2  [Open]   image for source color
         3  click on image to get source color
         4  [Open]   image for target color
         5  click on image to set target color
                             [done] [cancel]
*/

   zdialog *zd = zdialog_new(title,mWin,Bdone,Bcancel,null);               //  match_color dialog
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=2");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=3");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=3");
   zdialog_add_widget(zd,"label","labn1","vb1","1");
   zdialog_add_widget(zd,"label","labn2","vb1","2");
   zdialog_add_widget(zd,"label","labn3","vb1","3");
   zdialog_add_widget(zd,"label","labn4","vb1","4");
   zdialog_add_widget(zd,"label","labn5","vb1","5");
   zdialog_add_widget(zd,"hbox","hbrad","vb2");
   zdialog_add_widget(zd,"spin","radius","hbrad","1|20|1|10","space=5");
   zdialog_add_widget(zd,"label","labrad","hbrad",ZTX("mouse radius for color sample"));
   zdialog_add_widget(zd,"hbox","hbop1","vb2");
   zdialog_add_widget(zd,"button","open1","hbop1",ZTX("Open"),"space=5");
   zdialog_add_widget(zd,"label","labop1","hbop1",ZTX("image for source color"));
   zdialog_add_widget(zd,"hbox","hbclik1","vb2");
   zdialog_add_widget(zd,"label","labclik1","hbclik1",ZTX("click on image to get source color"));
   zdialog_add_widget(zd,"hbox","hbop2","vb2");
   zdialog_add_widget(zd,"button","open2","hbop2",ZTX("Open"),"space=5");
   zdialog_add_widget(zd,"label","labop2","hbop2",ZTX("image to set matching color"));
   zdialog_add_widget(zd,"hbox","hbclik2","vb2");
   zdialog_add_widget(zd,"label","labclik2","hbclik2",ZTX("click on image to set matching color"));

   zdialog_stuff(zd,"radius",match_color_radius);                          //  remember last radius
   zdialog_help(zd,"match_colors");                                        //  zdialog help topic        v.11.08
   zdialog_run(zd,match_color_dialog_event);                               //  run dialog - parallel

   EFmatchcolor.funcname = "match-color";
   EFmatchcolor.Farea = 1;                                                 //  select area ignored
   EFmatchcolor.threadfunc = match_color_thread;
   EFmatchcolor.mousefunc = match_color_mousefunc;

   match_color_mode = 0;
   if (curr_file) {
      match_color_mode = 1;                                                //  image 1 ready to click
      takeMouse(zd,match_color_mousefunc,0);                               //  connect mouse function
   }

   return;
}


//  match_color dialog event and completion function

int match_color_dialog_event(zdialog *zd, const char *event)               //  match_color dialog event function
{
   int      err;
   
   if (zd->zstat)                                                          //  end dialog
   {
      if (match_color_mode == 4) {                                         //  edit was started
         if (zd->zstat == 1) edit_done(EFmatchcolor);
         else edit_cancel(EFmatchcolor);
      }
      freeMouse();
      zdialog_free(zd);
      match_color_mode = 0;
      return 1;
   }
   
   if (match_color_mode == 4)                                              //  if prior edit, cancel it
      edit_cancel(EFmatchcolor);

   if (strEqu(event,"radius"))                                             //  set new mouse radius
      zdialog_fetch(zd,"radius",match_color_radius);
   
   if (strEqu(event,"open1"))                                              //  get image 1 for color source
   {
      match_color_mode = 0;
      err = f_open(null,0,0);
      if (! err) match_color_mode = 1;                                     //  image 1 ready to click
   }

   if (strEqu(event,"open2"))                                              //  get image 2 to set matching color
   {
      if (match_color_mode < 2) {
         zmessageACK(mWin,ZTX("select source image color first"));         //  check that RGB1 has been set
         return 1;
      }
      match_color_mode = 2;
      err = f_open(null,0,0);
      if (err) return 1;
      match_color_mode = 3;                                                //  image 2 ready to click
   }

   takeMouse(zd,match_color_mousefunc,0);                                  //  reconnect mouse function
   return 1;
}


//  mouse function - click on image and get colors to match

void match_color_mousefunc()
{
   void  match_color_getRGB(int px, int py, int RGB[3]);

   int      px, py;
   zdialog  *zd = EFmatchcolor.zd;

   if (match_color_mode < 1) return;                                       //  no image available yet
   
   toparcx = Mxposn - match_color_radius;                                  //  mouse outline circle
   toparcy = Myposn - match_color_radius;
   toparcw = toparch = 2 * match_color_radius;
   Ftoparc = 1;
   paint_toparc(3);
   
   if (LMclick)
   {
      LMclick = 0;
      px = Mxclick;
      py = Myclick;
   
      if (match_color_mode == 1 || match_color_mode == 2)                  //  image 1 ready to click
      {
         match_color_getRGB(px,py,match_color_RGB1);                       //  get RGB1 color
         match_color_mode = 2;
         return;
      }
      
      if (match_color_mode == 3 || match_color_mode == 4)                  //  image 2 ready to click
      {
         match_color_getRGB(px,py,match_color_RGB2);                       //  get RGB2 color

         if (match_color_mode == 4) {
            edit_cancel(EFmatchcolor);                                     //  if prior edit, cancel it
            takeMouse(zd,match_color_mousefunc,0);                         //  reconnect mouse function
            match_color_mode = 3;
         }

         if (! edit_setup(EFmatchcolor)) return;                           //  setup edit - thread will launch
         match_color_mode = 4;                                             //  edit waiting for cancel or done
         signal_thread();                                                  //  update the target image
         return;
      }
   }

   return;
}


//  get the RGB averages for pixels within mouse radius

void match_color_getRGB(int px, int py, int RGB[3])
{
   int      rad1 = match_color_radius;
   int      rad2 = rad1 * rad1;
   int      rad, npix, qx, qy;
   int      red, green, blue;
   uint16   *pix1;
   PXM      *pxm;

   pxm = f_load(curr_file,16);
   if (! pxm) return;

   npix = 0;   
   red = green = blue = 0;

   for (qy = py-rad1; qy <= py+rad1; qy++)
   for (qx = px-rad1; qx <= px+rad1; qx++)
   {
      if (qx < 0 || qx > Fww-1) continue;
      if (qy < 0 || qy > Fhh-1) continue;
      rad = (qx-px) * (qx-px) + (qy-py) * (qy-py);
      if (rad > rad2) continue;
      pix1 = PXMpix(pxm,qx,qy);
      red += pix1[0];
      green += pix1[1];
      blue += pix1[2];
      npix++;
   }
   
   RGB[0] = red / npix;
   RGB[1] = green / npix;
   RGB[2] = blue / npix;
   
   PXM_free(pxm);
   return;
}


//  thread function - start multiple working threads

void * match_color_thread(void *)
{
   void  * match_color_wthread(void *arg);                                 //  worker thread
   
   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request
      
      for (int ii = 0; ii < Nwt; ii++)                                     //  start worker threads
         start_wthread(match_color_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion

      CEF->Fmod = 1;
      mwpaint2();                                                          //  update window
   }

   return 0;                                                               //  not executed, avoid warning
}


void * match_color_wthread(void *arg)                                      //  worker thread function
{
   int         index = *((int *) (arg));
   int         px, py;
   uint16      *pix3;
   double      Rred, Rgreen, Rblue;
   double      red, green, blue, cmax;
   
   Rred = 1.0 * match_color_RGB1[0] / match_color_RGB2[0];                 //  color adjustment ratios
   Rgreen = 1.0 * match_color_RGB1[1] / match_color_RGB2[1];
   Rblue = 1.0 * match_color_RGB1[2] / match_color_RGB2[2];

   for (py = index; py < E3hh; py += Nwt)                                  //  loop all image pixels
   for (px = 0; px < E3ww; px++)
   {
      pix3 = PXMpix(E3pxm16,px,py);
      
      red = pix3[0] * Rred;                                                //  adjust colors
      green = pix3[1] * Rgreen;
      blue = pix3[2] * Rblue;
      
      cmax = red;                                                          //  check for overflow
      if (green > cmax) cmax = green;
      if (blue > cmax) cmax = blue;

      if (cmax > 65535) {                                                  //  fix overflow
         red = red * 65535 / cmax;
         green = green * 65535 / cmax;
         blue = blue * 65535 / cmax;
      }      

      pix3[0] = red;
      pix3[1] = green;
      pix3[2] = blue;
   }

   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


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

//  DRGB menu function
//  Adjust Density (Brightness) and RGB levels using 0.01 OD units

editfunc    EFdrgb;                                                        //  edit function data
int         DRGBinputs[8];
int         DRGBbias = 0;
char        *DRGB_file = 0;

void m_DRGB(GtkWidget *, const char *)                                     //  new v.11.08
{
   int    DRGB_dialog_event(zdialog *zd, cchar *event);
   void * DRGB_thread(void *);

   zfuncs::F1_help_topic = "DRGB";
   
   EFdrgb.funcname = "DRGB";                                               //  function name
   EFdrgb.Fprev = 1;                                                       //  use preview
   EFdrgb.Farea = 2;                                                       //  select area usable
   EFdrgb.Fpara = 1;                                                       //  parallel edit OK
   EFdrgb.threadfunc = DRGB_thread;                                        //  thread function
   if (! edit_setup(EFdrgb)) return;                                       //  setup edit
   
/***
    _________________________________________________
   |                                                 |
   |  [reset]   [x] Add standard bias                |
   |  +Brightness -Density [__|v]   Contrast [__|v]  |
   |  +Red -Cyan           [__|v]     Red    [__|v]  |
   |  +Green -Magenta      [__|v]     Green  [__|v]  |
   |  +Blue -Yellow        [__|v]     Blue   [__|x]  |
   |                                                 |
   |                  [open] [save] [done] [cancel]  |
   |_________________________________________________|
   
***/

   zdialog *zd = zdialog_new("DRGB",mWin,Bopen,Bsave,Bdone,Bcancel,null);  //  new dialog
   zdialog_help(zd,"DRGB");                                                //  add help topic to zdialog
   EFdrgb.zd = zd;                                                         //  add dialog to edit function

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","reset","hb1",Breset,"space=10");
   zdialog_add_widget(zd,"check","bias","hb1",ZTX("Add standard bias"),"space=5");
   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb2",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb2",0,"homog|space=5");
   zdialog_add_widget(zd,"label","space","hb2",0,"space=8");
   zdialog_add_widget(zd,"vbox","vb3","hb2",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb4","hb2",0,"homog|space=5");
   zdialog_add_widget(zd,"button","buBriteDens","vb1",ZTX("+Brightness -Density"));
   zdialog_add_widget(zd,"button","buRedDens","vb1",ZTX("+Red -Cyan"));
   zdialog_add_widget(zd,"button","buGreenDens","vb1",ZTX("+Green -Magenta"));
   zdialog_add_widget(zd,"button","buBlueDens","vb1",ZTX("+Blue -Yellow"));
   zdialog_add_widget(zd,"spin","spBriteDens","vb2","-99|+99|1|0");
   zdialog_add_widget(zd,"spin","spRedDens","vb2","-99|+99|1|0");
   zdialog_add_widget(zd,"spin","spGreenDens","vb2","-99|+99|1|0");
   zdialog_add_widget(zd,"spin","spBlueDens","vb2","-99|+99|1|0");
   zdialog_add_widget(zd,"button","buContrast","vb3",ZTX("Contrast"));
   zdialog_add_widget(zd,"button","buRedCon","vb3",ZTX("Red"));
   zdialog_add_widget(zd,"button","buGreenCon","vb3",ZTX("Green"));
   zdialog_add_widget(zd,"button","buBlueCon","vb3",ZTX("Blue"));
   zdialog_add_widget(zd,"spin","spContrast","vb4","-99|+99|1|0");
   zdialog_add_widget(zd,"spin","spRedCon","vb4","-99|+99|1|0");
   zdialog_add_widget(zd,"spin","spGreenCon","vb4","-99|+99|1|0");
   zdialog_add_widget(zd,"spin","spBlueCon","vb4","-99|+99|1|0");

   if (! DRGB_file) {                                                      //  default file
      DRGB_file = zmalloc(200);                                            //  /home/<user>/.fotoxx/DRGB_parameters
      snprintf(DRGB_file,200,"%s/DRGB_parameters",get_zuserdir());
   }

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


//  DRGB dialog event and completion function

int DRGB_dialog_event(zdialog *zd, cchar *event)                           //  DRGB dialog event function
{
   void  DRGB_open();
   void  DRGB_save();
   
   int      bias;
   
   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                //  open file
         zd->zstat = 0;                                                    //  keep dialog active
         DRGB_open();                                                      //  read file

         zdialog_stuff(zd,"bias",DRGBbias);                                //  set all data
         zdialog_stuff(zd,"spBriteDens",DRGBinputs[0]);
         zdialog_stuff(zd,"spRedDens",DRGBinputs[1]);
         zdialog_stuff(zd,"spGreenDens",DRGBinputs[2]);
         zdialog_stuff(zd,"spBlueDens",DRGBinputs[3]);
         zdialog_stuff(zd,"spContrast",DRGBinputs[4]);
         zdialog_stuff(zd,"spRedCon",DRGBinputs[5]);
         zdialog_stuff(zd,"spGreenCon",DRGBinputs[6]);
         zdialog_stuff(zd,"spBlueCon",DRGBinputs[7]);
      }

      else if (zd->zstat == 2) {                                           //  save file
         zd->zstat = 0;                                                    //  keep dialog active
         DRGB_save();                                                      //  write file
      }

      else if (zd->zstat == 3) edit_done(EFdrgb);                          //  done

      else edit_cancel(EFdrgb);                                            //  cancel or destroy

      return 0;
   }

   edit_takeover(EFdrgb);                                                  //  set parallel edit function
   
   if (strEqu(event,"bias"))                                               //  bias was changed
   {
      zdialog_fetch(zd,"bias",bias);
      if (bias != DRGBbias) 
      {
         if (bias) {                                                       //  off to on
            DRGBinputs[0] += 72;
            DRGBinputs[2] += 32;
            DRGBinputs[3] += 32;
            zdialog_set_limits(zd,"spBriteDens",-27,+171);
            zdialog_set_limits(zd,"spGreenDens",-67,+131);
            zdialog_set_limits(zd,"spBlueDens",-67,+131);
         }
         else {                                                            //  on to off
            DRGBinputs[0] -= 72;
            DRGBinputs[2] -= 32;
            DRGBinputs[3] -= 32;
            zdialog_set_limits(zd,"spBriteDens",-99,+99);
            zdialog_set_limits(zd,"spGreenDens",-99,+99);
            zdialog_set_limits(zd,"spBlueDens",-99,+99);
         }
         zdialog_stuff(zd,"spBriteDens",DRGBinputs[0]);
         zdialog_stuff(zd,"spGreenDens",DRGBinputs[2]);
         zdialog_stuff(zd,"spBlueDens",DRGBinputs[3]);
      }
      DRGBbias = bias;
      return 1;
   }
   
   if (strEqu(event,"reset"))                                              //  reset dialog controls
   {
      DRGBinputs[0] = DRGBbias * 72;
      DRGBinputs[1] = DRGBbias * 0;
      DRGBinputs[2] = DRGBbias * 32;
      DRGBinputs[3] = DRGBbias * 32;
      DRGBinputs[4] = 0;
      DRGBinputs[5] = 0;
      DRGBinputs[6] = 0;
      DRGBinputs[7] = 0;
      zdialog_stuff(zd,"spBriteDens",DRGBinputs[0]);
      zdialog_stuff(zd,"spRedDens",DRGBinputs[1]);
      zdialog_stuff(zd,"spGreenDens",DRGBinputs[2]);
      zdialog_stuff(zd,"spBlueDens",DRGBinputs[3]);
      zdialog_stuff(zd,"spContrast",DRGBinputs[4]);
      zdialog_stuff(zd,"spRedCon",DRGBinputs[5]);
      zdialog_stuff(zd,"spGreenCon",DRGBinputs[6]);
      zdialog_stuff(zd,"spBlueCon",DRGBinputs[7]);
   }

   zdialog_fetch(zd,"spBriteDens",DRGBinputs[0]);                          //  get all inputs
   zdialog_fetch(zd,"spRedDens",DRGBinputs[1]);
   zdialog_fetch(zd,"spGreenDens",DRGBinputs[2]);
   zdialog_fetch(zd,"spBlueDens",DRGBinputs[3]);
   zdialog_fetch(zd,"spContrast",DRGBinputs[4]);
   zdialog_fetch(zd,"spRedCon",DRGBinputs[5]);
   zdialog_fetch(zd,"spGreenCon",DRGBinputs[6]);
   zdialog_fetch(zd,"spBlueCon",DRGBinputs[7]);

   signal_thread();                                                        //  trigger update thread
   wait_thread_idle();                                                     //  wait until done (optional)
   return 1;
}


//  load DRGB parameters from a previously saved file

void DRGB_open()
{
   int  DRGB_open_dialog_event(zdialog *zd, cchar *event);

   zdialog     *zd;
   
   zd = zdialog_new(ZTX("Load DRGB parameters"),mWin,Bopen,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",ZTX("File:"),"space=3");
   zdialog_add_widget(zd,"entry","file","hb1",0,"expand|scc=40");
   zdialog_add_widget(zd,"button","browse","hb1",Bbrowse,"space=5");
   
   zdialog_stuff(zd,"file",DRGB_file);
   
   zdialog_run(zd,DRGB_open_dialog_event);
   zdialog_wait(zd);
   return;
}


int DRGB_open_dialog_event(zdialog *zd, cchar *event)
{
   char     *pp, file[200];
   FILE     *fid = 0;
   int      zstat, nn;
   
   if (strEqu(event,"browse")) {
      pp = zgetfile1(ZTX("DRGB parameters file"),"open",DRGB_file);
      if (! pp) return 0;
      zdialog_stuff(zd,"file",pp);
      zfree(pp);
   }
   
   zstat = zd->zstat;                                                      //  completion button
   
   if (zstat)
   {
      if (zstat == 1)                                                      //  open
      {
         zdialog_fetch(zd,"file",file,200);                                //  get file from dialog
         if (DRGB_file) zfree(DRGB_file);
         DRGB_file = strdupz(file);

         fid = fopen(file,"r");                                            //  open file
         if (! fid) {
            zmessageACK(mWin,ZTX("file not found"));
            return 1;
         }
         
         nn = fscanf(fid,"DRGB parameters");                               //  read header
         
         nn = fscanf(fid,"%4d %4d %4d %4d %4d %4d %4d %4d %4d",            //  read data
                           &DRGBbias, 
                           &DRGBinputs[0], &DRGBinputs[1], &DRGBinputs[2],
                           &DRGBinputs[3], &DRGBinputs[4], &DRGBinputs[5], 
                           &DRGBinputs[6], &DRGBinputs[7]);

         if (nn != 9) zmessageACK(mWin,"file format error");

         fclose(fid);
         zdialog_free(zd);
      }
      
      else zdialog_free(zd);                                               //  cancel or [x]
   }

   return 1;
}


//  save DRGB parameters to a file

void DRGB_save()
{
   int  DRGB_save_dialog_event(zdialog *zd, cchar *event);

   zdialog     *zd;
   
   zd = zdialog_new(ZTX("Save DRGB parameters"),mWin,Bsave,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",ZTX("File:"),"space=3");
   zdialog_add_widget(zd,"entry","file","hb1",0,"expand|scc=40");
   zdialog_add_widget(zd,"button","browse","hb1",Bbrowse,"space=5");
   
   zdialog_stuff(zd,"file",DRGB_file);
   
   zdialog_run(zd,DRGB_save_dialog_event);
   zdialog_wait(zd);

   return;
}


int DRGB_save_dialog_event(zdialog *zd, cchar *event)
{
   char     *pp, file[200];
   FILE     *fid = 0;
   int      zstat;
   
   if (strEqu(event,"browse")) {
      pp = zgetfile1(ZTX("DRGB parameters file"),"save",DRGB_file);
      if (! pp) return 0;
      zdialog_stuff(zd,"file",pp);
      zfree(pp);
   }
   
   zstat = zd->zstat;                                                      //  completion button
   
   if (zstat)
   {
      if (zstat == 1)                                                      //  save
      {
         zdialog_fetch(zd,"file",file,200);                                //  get file from dialog
         if (DRGB_file) zfree(DRGB_file);
         DRGB_file = strdupz(file);

         fid = fopen(file,"w");                                            //  open file
         if (! fid) {
            zmessageACK(mWin,strerror(errno));
            return 1;
         }
         
         fprintf(fid,"DRGB parameters \n");                                //  write headers

         fprintf(fid,"%4d %4d %4d %4d %4d %4d %4d %4d %4d ",               //  write data
                              DRGBbias, 
                              DRGBinputs[0], DRGBinputs[1], DRGBinputs[2],
                              DRGBinputs[3], DRGBinputs[4], DRGBinputs[5], 
                              DRGBinputs[6], DRGBinputs[7]);
         fprintf(fid,"\n");
         fclose(fid);
         zdialog_free(zd);
      }

      else zdialog_free(zd);                                               //  cancel or [x]
   }

   return 1;
}


//  thread function - multiple working threads to update image

void * DRGB_thread(void *)
{
   void  * DRGB_wthread(void *arg);                                        //  worker thread

   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request
      
      if (Factivearea) SB_goal = sa_Npixel;                                //  set up progress monitor
      else  SB_goal = E3ww * E3hh;
      SB_done = 0;

      for (int ii = 0; ii < Nwt; ii++)                                     //  start worker threads
         start_wthread(DRGB_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion

      SB_goal = SB_done = 0;
      CEF->Fmod = 1;                                                       //  image modified
      mwpaint2();                                                          //  update window
   }

   return 0;                                                               //  not executed, stop warning
}


void * DRGB_wthread(void *arg)                                             //  worker thread function
{
   double      Du, Ru, Gu, Bu, Rd, Gd, Bd;
   double      con, conR, conG, conB;
   double      R1, G1, B1, R3, G3, B3;

   int         index = *((int *) (arg));
   int         px, py, ii, dist = 0;
   uint16      *pix1, *pix3;
   double      cmax, F, f1, f2;
   double      c1 = 100.0 / 65536.0;
   double      c2 = 2.0 + log10(1.0/c1);
   double      c3 = 1.0 / 65536.0;
   
   #define ODfunc(rgb) (2.0 - log10(c1 * rgb))                             //  RGB units (1-65536) to OD units
   #define RGBfunc(od) (pow(10,(c2 - od)))                                 //  OD units to RGB units
   
   Du = -0.01 * DRGBinputs[0];                                             //  convert inputs from cc to OD units
   Ru = -0.01 * DRGBinputs[1];                                             //  range is -0.99 to +0.99
   Gu = -0.01 * DRGBinputs[2];
   Bu = -0.01 * DRGBinputs[3];
   
   if (DRGBbias) {                                                         //  remove bias
      Du += 0.72;
      Gu += 0.32;
      Bu += 0.32;
   }
   
   con = -0.01 * DRGBinputs[4];
   conR = -0.01 * DRGBinputs[5];
   conG = -0.01 * DRGBinputs[6];
   conB = -0.01 * DRGBinputs[7];
   
   for (py = index; py < E3hh; py += Nwt)                                  //  loop all image pixels
   for (px = 0; px < E3ww; px++)
   {
      if (Factivearea) {                                                   //  select area active
         ii = py * E3ww + px;
         dist = sa_pixmap[ii];                                             //  distance from edge
         if (! dist) continue;                                             //  outside area
      }
      
      pix1 = PXMpix(E1pxm16,px,py);                                        //  input pixel
      pix3 = PXMpix(E3pxm16,px,py);                                        //  output pixel
      
      R1 = pix1[0] + 1;                                                    //  input RGB values, 0-65535
      G1 = pix1[1] + 1;                                                    //  (avoid 0)
      B1 = pix1[2] + 1;
      
      Rd = ODfunc(R1);                                                     //  convert to OD units
      Gd = ODfunc(G1);
      Bd = ODfunc(B1);
      
      Rd += Du + Ru;                                                       //  add user OD increments
      Gd += Du + Gu;
      Bd += Du + Bu;
      
      if (con) {                                                           //  overall contrast
         Rd += con * c3 * (R1-32768);
         Gd += con * c3 * (G1-32768);
         Bd += con * c3 * (B1-32768);
      }

      if (conR)                                                            //  single color contrast
         Rd += conR * c3 * (R1-32768);
      if (conG) 
         Gd += conG * c3 * (G1-32768);
      if (conB) 
         Bd += conB * c3 * (B1-32768);
      
      R3 = RGBfunc(Rd);                                                    //  convert back to RGB units
      G3 = RGBfunc(Gd);
      B3 = RGBfunc(Bd);
      
      if (R3 < 0) R3 = 0;                                                  //  stop underflow
      if (G3 < 0) G3 = 0;
      if (B3 < 0) B3 = 0;

      if (R3 > 65535 || G3 > 65535 || B3 > 65535) {                        //  stop overflow
         cmax = R3;
         if (G3 > cmax) cmax = G3;
         if (B3 > cmax) cmax = B3;
         F = 65535 / cmax;
         R3 *= F;
         G3 *= F;
         B3 *= F;
      }

      if (Factivearea && dist < sa_blend) {                                //  select area is active,
         f1 = 1.0 * dist / sa_blend;                                       //    blend changes over sa_blend
         f2 = 1.0 - f1;
         R3 = f1 * R3 + f2 * R1;
         G3 = f1 * G3 + f2 * G1;
         B3 = f1 * B3 + f2 * B1;
      }
      
      pix3[0] = R3;                                                        //  output RGB values
      pix3[1] = G3;
      pix3[2] = B3;
   }

   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


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

//  Show RGB values for 1-9 pixels selected with mouse-clicks.
//  Revise RGB values for the selected points and then revise all other 
//  pixels to match, using a distance-weighted average of the selected pixels.

void  RGBR_dialog();
void  RGBR_mousefunc();
void  RGBR_stuff();
void * RGBR_thread(void *);

zdialog  *RGBRzd; 
int      RGBRpixel[9][2];                                                  //  last 9 pixel locations clicked
double   RGBRval1[9][3];                                                   //  original RGB values, 0 to 255.9
double   RGBRval3[9][3];                                                   //  revised RGB values, 0 to 255.9
int      RGBRmetric = 1;                                                   //  1/2/3 = /RGB/EV/OD
int      RGBRdelta = 0;                                                    //  flag, delta mode
int      RGBRrestart;                                                      //  flag, restart dialog
int      RGBRnpix;                                                         //  no. of selected pixels
double   RGBRtime;                                                         //  last click time
int      RGBRblend;                                                        //  blend factor, 10 to 1000
editfunc EFrgbrevise;                                                      //  edit function parameters

#define RGB2EV(rgb) (log2(rgb)-7)                                          //  RGB 0.1 to 255.9 >> EV -10 to +1
#define EV2RGB(ev) (pow(2,ev+7))                                           //  inverse
#define RGB2OD(rgb) (2-log10(100*rgb/256))                                 //  RGB 0.1 to 255.9 >> OD 3.4 to 0.000017
#define OD2RGB(od) (pow(10,2.40824-od))                                    //  inverse


void m_revise_RGB(GtkWidget *, cchar *menu)                                //  new        v.11.07
{
   int   RGBR_event(zdialog *zd, cchar *event);

   GtkWidget   *widget;
   cchar       *limits;
   zdialog     *zd;
   cchar       *mess = ZTX("Click image to select pixels.");

   PangoFontDescription    *widgetfont;
   widgetfont = pango_font_description_from_string("Monospace 8");         //  small monospace font for widgets

   zfuncs::F1_help_topic = "revise_RGB";
   
   EFrgbrevise.funcname = "revise_RGB";                                    //  setup edit function
   EFrgbrevise.threadfunc = RGBR_thread;
   EFrgbrevise.mousefunc = RGBR_mousefunc;
   EFrgbrevise.Farea = 1;
   if (! edit_setup(EFrgbrevise)) return;

   RGBRblend = 20;                                                         //  default slider value
   RGBRnpix = 0;                                                           //  no pixels selected yet
   
/***
   
      Click image to select pixels.
      [x] delta
      Metric: (o) RGB  (o) EV  (o) OD
      Pixel          Red      Green    Blue
      A xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v]                             //  spin buttons for RGB/EV/OD values
      B xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v]
      C xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v] 
      D xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v] 
      E xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v] 
      F xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v] 
      G xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v] 
      H xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v] 
      I xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v] 
      Blend  ==============[]================

                     [reset] [done] [cancel]
***/
   
restart:

   zd = zdialog_new(ZTX("Revise RGB"),mWin,Breset,Bdone,Bcancel,null);
   EFrgbrevise.zd = zd;
   RGBRzd = zd;

   zdialog_add_widget(zd,"hbox","hbmess","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labmess","hbmess",mess,"space=5");

   zdialog_add_widget(zd,"hbox","hbmym","dialog");
   zdialog_add_widget(zd,"check","delta","hbmym","delta","space=5");
   zdialog_stuff(zd,"delta",RGBRdelta);

   zdialog_add_widget(zd,"hbox","hbmetr","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labmetr","hbmetr",ZTX("Metric:"),"space=5");
   zdialog_add_widget(zd,"radio","radRGB","hbmetr","RGB","space=3");
   zdialog_add_widget(zd,"radio","radEV","hbmetr","EV","space=3");
   zdialog_add_widget(zd,"radio","radOD","hbmetr","OD","space=3");
   
   if (RGBRmetric == 1) zdialog_stuff(zd,"radRGB",1);                      //  set current metric
   if (RGBRmetric == 2) zdialog_stuff(zd,"radEV",1);
   if (RGBRmetric == 3) zdialog_stuff(zd,"radOD",1);

   zdialog_add_widget(zd,"hbox","hbdata","dialog");
   zdialog_add_widget(zd,"vbox","vbpix","hbdata",0,"space=3|homog");       //  vbox for pixel locations
   zdialog_add_widget(zd,"hbox","hbpix","vbpix");
   zdialog_add_widget(zd,"label","labpix","hbpix","Pixel");                //  header
   
   char hbpixx[8] = "hbpixx", pixx[8] = "pixx";

   for (int ii = 1; ii < 10; ii++) {                                       //  add labels for pixel locations
      hbpixx[5] = '0' + ii;
      pixx[3] = '0' + ii;
      zdialog_add_widget(zd,"hbox",hbpixx,"vbpix");                        //  add hbox "hbpix1" to "hbpix9"
      zdialog_add_widget(zd,"label",pixx,hbpixx);                          //  add label "pix1" to "pix9"
      widget = zdialog_widget(zd,pixx);
      gtk_widget_modify_font(widget,widgetfont);                           //  use monofont
   }

   if (RGBRmetric == 1) limits = "-255.9|255.9|0.1|0.0";                   //  metric = RGB
   else if (RGBRmetric == 2) limits = "-8|8|0.01|0.0";                     //           EV
   else limits = "-3|3|0.002|0.0";                                         //           OD

   zdialog_add_widget(zd,"vbox","vbdat","hbdata",0,"space=3|homog");       //  vbox for pixel RGB values
   zdialog_add_widget(zd,"hbox","hbrgb","vbdat",0,"homog");
   zdialog_add_widget(zd,"label","labr","hbrgb",Bred);                     //  v.11.08
   zdialog_add_widget(zd,"label","labg","hbrgb",Bgreen);
   zdialog_add_widget(zd,"label","labb","hbrgb",Bblue);

   char hbdatx[8] = "hbdatx", redx[8] = "redx", greenx[8] = "greenx", bluex[8] = "bluex";

   for (int ii = 1; ii < 10; ii++) {
      hbdatx[5] = '0' + ii;
      redx[3] = '0' + ii;
      greenx[5] = '0' + ii;
      bluex[4] = '0' + ii;
      zdialog_add_widget(zd,"hbox",hbdatx,"vbdat");                        //  add hbox "hbdat1" to "hbdat9"
      zdialog_add_widget(zd,"spin",redx,hbdatx,limits,"space=3");          //  add spin buttons for "red1" to "red9" etc.
      zdialog_add_widget(zd,"spin",greenx,hbdatx,limits,"space=3");
      zdialog_add_widget(zd,"spin",bluex,hbdatx,limits,"space=3");
      widget = zdialog_widget(zd,redx);
      gtk_widget_modify_font(widget,widgetfont);                           //  use monofont
      widget = zdialog_widget(zd,greenx);
      gtk_widget_modify_font(widget,widgetfont);
      widget = zdialog_widget(zd,bluex);
      gtk_widget_modify_font(widget,widgetfont);
   }
   
   zdialog_add_widget(zd,"hbox","hbsoft","dialog","space=5");
   zdialog_add_widget(zd,"label","labsoft","hbsoft",ZTX("Blend"),"space=3");
   zdialog_add_widget(zd,"hscale","blend","hbsoft","0|100|1|20","space=5|expand");
   zdialog_add_widget(zd,"label","softval","hbsoft","20");

   RGBR_stuff();                                                           //  stuff current pixels (if restart)

   takeMouse(zd,RGBR_mousefunc,dragcursor);                                //  connect mouse function
   zdialog_help(zd,"revise_RGB");                                          //  zdialog help topic        v.11.08
   zdialog_run(zd,RGBR_event,"save");                                      //  run dialog
   zdialog_wait(zd);                                                       //  wait for completion
   if (RGBRrestart) goto restart;                                          //  restart dialog

   return;
}


//  dialog event function

int RGBR_event(zdialog *zd, cchar *event)
{
   int         button;
   int         ii, jj;
   char        text[8];
   double      val1, val3;
   
   if (zd->zstat) 
   {
      if (zd->zstat == 1) {                                                //  Reset
         zd->zstat = 0;                                                    //  keep dialog active
         RGBRnpix = 0;                                                     //  no pixels selected yet
         RGBR_stuff();                                                     //  clear dialog
         edit_reset();                                                     //  reset edits            v.11.08
         return 0;
      }
      else if (zd->zstat == 2)                                             //  Done
         edit_done(EFrgbrevise);
      else edit_cancel(EFrgbrevise);                                       //  Cancel or [x]
      freeMouse();                                                         //  disconnect mouse function
      zdialog_free(zd);                                                    //  kill dialog
      RGBRzd = 0;
      RGBRrestart = 0;
      erase_toptext(101);                                                  //  erase pixel labels from image
      return 1;
   }
   
   if (strEqu(event,"focus"))                                              //  toggle mouse capture
      takeMouse(zd,RGBR_mousefunc,dragcursor);                             //  connect mouse function
   
   if (strEqu(event,"blend")) {                                            //  new blend factor
      zdialog_fetch(zd,"blend",RGBRblend);
      sprintf(text,"%d",RGBRblend);                                        //  numeric feedback
      zdialog_stuff(zd,"softval",text);
      signal_thread();
      return 1;
   }

   if (strnEqu(event,"rad",3))                                             //  metric was changed
   {
      if (strEqu(event,"radRGB")) {                                        //  RGB
         zdialog_fetch(zd,event,button);
         if (button) RGBRmetric = 1;
      }
         
      if (strEqu(event,"radEV")) {                                         //  EV
         zdialog_fetch(zd,event,button);
         if (button) RGBRmetric = 2;
      }
         
      if (strEqu(event,"radOD")) {                                         //  OD
         zdialog_fetch(zd,event,button);
         if (button) RGBRmetric = 3;
      }

      if (button) {      
         freeMouse();                                                      //  restart dialog with new limits
         zdialog_free(zd);
         RGBRzd = 0;
         RGBRrestart = 1;
      }

      return 1;
   }

   if (strEqu(event,"delta")) {                                            //  change absolute/delta mode
      zdialog_fetch(zd,"delta",RGBRdelta);
      freeMouse();                                                         //  restart dialog with new limits
      zdialog_free(zd);
      RGBRzd = 0;
      RGBRrestart = 1;
      return 1;
   }
   
   ii = -1;                                                                //  no RGB change yet

   if (strnEqu(event,"red",3)) {                                           //  red1 - red9 was changed
      ii = event[3] - '1';                                                 //  pixel index 0-8
      jj = 0;                                                              //  color = red
   }

   if (strnEqu(event,"green",5)) {                                         //  green1 - green9
      ii = event[5] - '1';
      jj = 1;
   }

   if (strnEqu(event,"blue",4)) {                                          //  blue1 - blue9
      ii = event[4] - '1';
      jj = 2;
   }
   
   if (ii >= 0 && ii < 9)                                                  //  RGB value was revised
   {
/*
      static int     ignore = 0;

      if (ignore) {
         ignore = 0;
         return 1;
      }

      ignore = 1;
*/

      val1 = RGBRval1[ii][jj];                                             //  original pixel RGB value
      if (RGBRmetric == 2) val1 = RGB2EV(val1);                            //  convert to EV or OD units
      if (RGBRmetric == 3) val1 = RGB2OD(val1);

      zdialog_fetch(zd,event,val3);                                        //  revised RGB/EV/OD value from dialog
      if (RGBRdelta) val3 += val1;                                         //  if delta mode, make absolute

      if (RGBRmetric == 2) val3 = EV2RGB(val3);                            //  convert EV/OD to RGB value
      if (RGBRmetric == 3) val3 = OD2RGB(val3);

      if (fabs(RGBRval3[ii][jj] - val3) < 0.001) return 1;                 //  ignore re-entry after change

      if (val3 < 0.1) val3 = 0.1;                                          //  limit RGB within 0.1 to 255.9
      if (val3 > 255.9) val3 = 255.9;

      RGBRval3[ii][jj] = val3;                                             //  new RGB value for pixel
      
      if (RGBRmetric == 2) val3 = RGB2EV(val3);                            //  convert RGB to EV/OD units
      if (RGBRmetric == 3) val3 = RGB2OD(val3);

      if (RGBRdelta) val3 -= val1;                                         //  absolute back to relative
      zdialog_stuff(zd,event,val3);                                        //  limited value back to dialog
      
      signal_thread();                                                     //  signal thread to update image
   }

   return 1;
}


//  mouse function

void RGBR_mousefunc()                                                      //  mouse function
{
   int         ii;
   uint16      *pix3;

   if (! LMclick) return;
   LMclick = RMclick = 0;

   RGBRtime = get_seconds();                                               //  mark time of pixel click
   
   if (RGBRnpix == 9) {                                                    //  if table is full (9 entries)    v.11.08
      for (ii = 1; ii < 9; ii++) {                                         //    move positions 1-8 up 
         RGBRpixel[ii-1][0] = RGBRpixel[ii][0];                            //      to fill positions 0-7
         RGBRpixel[ii-1][1] = RGBRpixel[ii][1];
      }
      RGBRnpix = 8;                                                        //  count is now 8 entries
   }
   
   ii = RGBRnpix;                                                          //  next table position to fill
   RGBRpixel[ii][0] = Mxclick;                                             //  newest pixel
   RGBRpixel[ii][1] = Myclick;
   
   pix3 = PXMpix(E3pxm16,Mxclick,Myclick);                                 //  get initial RGB values from
   RGBRval1[ii][0] = RGBRval3[ii][0] = pix3[0] / 256.0;                    //    modified image E3
   RGBRval1[ii][1] = RGBRval3[ii][1] = pix3[1] / 256.0;
   RGBRval1[ii][2] = RGBRval3[ii][2] = pix3[2] / 256.0;

   RGBRnpix++;                                                             //  up pixel count
   RGBR_stuff();                                                           //  stuff pixels and values into dialog
   return;
}


//  stuff dialog with current pixels and their RGB/EV/OD values

void RGBR_stuff()
{
   static char    lab1[9][4] = { "A", "B", "C", "D", "E", "F", "G", "H", "I" };
   static char    lab2[9][4] = { " A ", " B ", " C ", " D ", " E ", " F ", " G ", " H ", " I " };

   int         px, py;
   double      red1, green1, blue1, red3, green3, blue3;
   char        text[100], pixx[8] = "pixx";
   char        redx[8] = "redx", greenx[8] = "greenx", bluex[8] = "bluex";
   zdialog     *zd = RGBRzd;
   
   erase_toptext(101);                                                     //  erase prior labels from image
   
   for (int ii = 0; ii < 9; ii++)                                          //  loop slots 0-8
   {
      pixx[3] = '1' + ii;                                                  //  widget names "pix1" ... "pix9"
      redx[3] = '1' + ii;                                                  //  widget names "red1" ... "red9"
      greenx[5] = '1' + ii;
      bluex[4] = '1' + ii;

      px = RGBRpixel[ii][0];                                               //  next pixel to report
      py = RGBRpixel[ii][1];
      if (ii >= RGBRnpix) {                                                //  > last pixel selected
         zdialog_stuff(zd,pixx,"");
         zdialog_stuff(zd,redx,0);                                         //  blank pixel and zero values
         zdialog_stuff(zd,greenx,0);
         zdialog_stuff(zd,bluex,0);
         continue;
      }

      sprintf(text,"%s %4d %4d",lab1[ii],px,py);                           //  format pixel "A nnnn nnnn"
      zdialog_stuff(zd,pixx,text);                                         //  pixel >> widget
      
      add_toptext(101,px,py,lab2[ii],"Sans 8");                            //  paint label on image at pixel
      
      red1 = RGBRval1[ii][0];                                              //  original RGB values for pixel
      green1 = RGBRval1[ii][1];
      blue1 = RGBRval1[ii][2];
      red3 = RGBRval3[ii][0];                                              //  revised RGB values
      green3 = RGBRval3[ii][1];
      blue3 = RGBRval3[ii][2];
      
      if (RGBRmetric == 2) {                                               //  convert to EV units if needed
         red1 = RGB2EV(red1);
         green1 = RGB2EV(green1);
         blue1 = RGB2EV(blue1);
         red3 = RGB2EV(red3);
         green3 = RGB2EV(green3);
         blue3 = RGB2EV(blue3);
      }
      
      if (RGBRmetric == 3) {                                               //  or OD units
         red1 = RGB2OD(red1);
         green1 = RGB2OD(green1);
         blue1 = RGB2OD(blue1);
         red3 = RGB2OD(red3);
         green3 = RGB2OD(green3);
         blue3 = RGB2OD(blue3);
      }
      
      if (RGBRdelta) {                                                     //  dialog is delta mode
         red3 -= red1;
         green3 -= green1;
         blue3 -= blue1;
      }
      
      zdialog_stuff(zd,redx,red3);
      zdialog_stuff(zd,greenx,green3);
      zdialog_stuff(zd,bluex,blue3);
   }

   mwpaint2();                                                             //  refresh window
   return;
}


//  thread function - multiple working threads to update image

void * RGBR_thread(void *)
{
   void  * RGBR_wthread(void *arg);                                        //  worker thread

   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request
      zsleep(0.3);                                                         //  more time for dialog controls
      
      if (RGBRnpix < 1) continue;                                          //  must have 1+ pixels in table
      
      for (int ii = 0; ii < Nwt; ii++)                                     //  start worker threads
         start_wthread(RGBR_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion

      CEF->Fmod = 1;                                                       //  image modified
      mwpaint2();                                                          //  update window
   }

   return 0;                                                               //  not executed, stop warning
}


void * RGBR_wthread(void *arg)                                             //  worker thread function
{
   int         index = *((int *) (arg));
   int         px1, py1, px2, py2, ii;
   uint16      *pix1, *pix3;
   double      dist[9], weight[9], sumdist;
   double      blend, delta, red, green, blue, max;
   
   blend = Fww;
   if (Fhh > Fww) blend = Fhh;
   blend = blend * blend;
   blend = 0.0001 * RGBRblend * RGBRblend * blend + 100;                   //  100 to (image size)**2       v.11.08
   
   for (py1 = index; py1 < E3hh; py1 += Nwt)                               //  loop all image pixels
   for (px1 = 0; px1 < E3ww; px1++)
   {
      for (sumdist = ii = 0; ii < RGBRnpix; ii++)                          //  compute weight of each revision point
      {
         px2 = RGBRpixel[ii][0];
         py2 = RGBRpixel[ii][1];
         dist[ii] = (px1-px2)*(px1-px2) + (py1-py2)*(py1-py2);             //  distance (px1,py1) to (px2,py2)
         dist[ii] = 1.0 / (dist[ii] + blend);                              //  blend reduces peaks at revision points
         sumdist += dist[ii];                                              //  sum inverse distances
      }
      
      for (ii = 0; ii < RGBRnpix; ii++)                                    //  weight of each point
         weight[ii] = dist[ii] / sumdist;

      pix1 = PXMpix(E1pxm16,px1,py1);                                      //  input pixel
      pix3 = PXMpix(E3pxm16,px1,py1);                                      //  output pixel
      
      red = pix1[0];
      green = pix1[1];
      blue = pix1[2];
      
      for (ii = 0; ii < RGBRnpix; ii++) {                                  //  apply weighted color changes
         delta = RGBRval3[ii][0] - RGBRval1[ii][0];                        //    to each color
         red += weight[ii] * 256 * delta;
         delta = RGBRval3[ii][1] - RGBRval1[ii][1];
         green += weight[ii] * 256 * delta;
         delta = RGBRval3[ii][2] - RGBRval1[ii][2];
         blue += weight[ii] * 256 * delta;
      }
      
      max = red;
      if (green > max) max = green;
      if (blue > max) max = blue;

      if (max > 65535) {                                                   //  stop overflow/underflow
         red = red * 65535 / max;
         green = green * 65535 / max;
         blue = blue * 65535 / max;
      }
      
      if (red < 0) red = 0;
      if (green < 0) green = 0;
      if (blue < 0) blue = 0;
      
      pix3[0] = red;
      pix3[1] = green;
      pix3[2] = blue;
   }

   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


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

//  red eye removal function

struct sredmem {                                                           //  red-eye struct in memory
   char        type, space[3];
   int         cx, cy, ww, hh, rad, clicks;
   double      thresh, tstep;
};
sredmem  redmem[100];                                                      //  store up to 100 red-eyes

int      Nredmem = 0, maxredmem = 100;
void     redeye_mousefunc();

editfunc    EFredeye;


void m_redeye(GtkWidget *, cchar *)
{
   int      redeye_dialog_event(zdialog *zd, cchar *event);

   cchar    *redeye_message = ZTX(
               "Method 1:\n"
               "  Left-click on red-eye to darken.\n"
               "Method 2:\n"
               "  Drag down and right to enclose red-eye.\n"
               "  Left-click on red-eye to darken.\n"
               "Undo red-eye:\n"
               "  Right-click on red-eye.");

   zfuncs::F1_help_topic = "red_eye";                                      //  v.10.8

   EFredeye.funcname = "redeye";
   EFredeye.Farea = 1;                                                     //  select area ignored
   EFredeye.mousefunc = redeye_mousefunc;
   if (! edit_setup(EFredeye)) return;                                     //  setup edit

   zdialog *zd = zdialog_new(ZTX("Red Eye Reduction"),mWin,Bdone,Bcancel,null);
   EFredeye.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",redeye_message);
   zdialog_help(zd,"red_eye");                                             //  zdialog help topic        v.11.08
   zdialog_run(zd,redeye_dialog_event,"save");                             //  run dialog - parallel     v.11.07

   Nredmem = 0;
   takeMouse(zd,redeye_mousefunc,dragcursor);                              //  connect mouse function          v.11.03
   return;
}


//  dialog event and completion callback function

int redeye_dialog_event(zdialog *zd, cchar *event)
{
   if (zd->zstat) {
      if (Nredmem > 0) CEF->Fmod = 1;
      Ftoparc = ptoparc = 0;
      if (zd->zstat == 1) edit_done(EFredeye);
      else edit_cancel(EFredeye);
      return 0;
   }

   if (strEqu(event,"focus"))                                              //  toggle mouse capture         v.10.12
      takeMouse(zd,redeye_mousefunc,dragcursor);                           //  connect mouse function

   return 0;
}


//  mouse functions to define, darken, and undo red-eyes

int      redeye_createF(int px, int py);                                   //  create 1-click red-eye (type F)
int      redeye_createR(int px, int py, int ww, int hh);                   //  create robust red-eye (type R)
void     redeye_darken(int ii);                                            //  darken red-eye
void     redeye_distr(int ii);                                             //  build pixel redness distribution
int      redeye_find(int px, int py);                                      //  find red-eye at mouse position
void     redeye_remove(int ii);                                            //  remove red-eye at mouse position
int      redeye_radlim(int cx, int cy);                                    //  compute red-eye radius limit


void redeye_mousefunc()
{
   int         ii, px, py, ww, hh;

   if (Nredmem == maxredmem) {
      zmessageACK(mWin,"%d red-eye limit reached",maxredmem);              //  too many red-eyes
      return;
   }

   if (LMclick)                                                            //  left mouse click
   {
      LMclick = 0;

      px = Mxclick;                                                        //  click position
      py = Myclick;
      if (px < 0 || px > E3ww-1 || py < 0 || py > E3hh-1) return;          //  outside image area

      ii = redeye_find(px,py);                                             //  find existing red-eye
      if (ii < 0) ii = redeye_createF(px,py);                              //  or create new type F
      redeye_darken(ii);                                                   //  darken red-eye
   }
   
   if (RMclick)                                                            //  right mouse click
   {
      RMclick = 0;
      px = Mxclick;                                                        //  click position
      py = Myclick;
      ii = redeye_find(px,py);                                             //  find red-eye
      if (ii >= 0) redeye_remove(ii);                                      //  if found, remove
   }

   if (Mxdrag || Mydrag)                                                   //  mouse drag underway
   {
      px = Mxdown;                                                         //  initial position
      py = Mydown;
      ww = Mxdrag - Mxdown;                                                //  increment
      hh = Mydrag - Mydown;
      if (ww < 2 && hh < 2) return;
      if (ww < 2) ww = 2;
      if (hh < 2) hh = 2;
      if (px < 1) px = 1;                                                  //  keep within image area
      if (py < 1) py = 1;      
      if (px + ww > E3ww-1) ww = E3ww-1 - px;
      if (py + hh > E3hh-1) hh = E3hh-1 - py;
      ii = redeye_find(px,py);                                             //  find existing red-eye
      if (ii >= 0) redeye_remove(ii);                                      //  remove it
      ii = redeye_createR(px,py,ww,hh);                                    //  create new red-eye type R
   }

   mwpaint2();
   return;
}


//  create type F redeye (1-click automatic)

int redeye_createF(int cx, int cy)
{
   int         cx0, cy0, cx1, cy1, px, py, rad, radlim;
   int         loops, ii;
   int         Tnpix, Rnpix, R2npix;
   double      rd, rcx, rcy, redpart;
   double      Tsum, Rsum, R2sum, Tavg, Ravg, R2avg;
   double      sumx, sumy, sumr;
   uint16      *ppix;
   
   cx0 = cx;
   cy0 = cy;
   
   for (loops = 0; loops < 8; loops++)
   {
      cx1 = cx;
      cy1 = cy;

      radlim = redeye_radlim(cx,cy);                                       //  radius limit (image edge)
      Tsum = Tavg = Ravg = Tnpix = 0;

      for (rad = 0; rad < radlim-2; rad++)                                 //  find red-eye radius from (cx,cy)
      {
         Rsum = Rnpix = 0;
         R2sum = R2npix = 0;

         for (py = cy-rad-2; py <= cy+rad+2; py++)
         for (px = cx-rad-2; px <= cx+rad+2; px++)
         {
            rd = sqrt((px-cx)*(px-cx) + (py-cy)*(py-cy));
            ppix = PXMpix(E3pxm16,px,py);
            redpart = pixred(ppix);

            if (rd <= rad + 0.5 && rd > rad - 0.5) {                       //  accum. redness at rad
               Rsum += redpart;
               Rnpix++;
            }
            else if (rd <= rad + 2.5 && rd > rad + 1.5) {                  //  accum. redness at rad+2
               R2sum += redpart;
               R2npix++;
            }
         }
         
         Tsum += Rsum;
         Tnpix += Rnpix;
         Tavg = Tsum / Tnpix;                                              //  avg. redness over 0-rad
         Ravg = Rsum / Rnpix;                                              //  avg. redness at rad
         R2avg = R2sum / R2npix;                                           //  avg. redness at rad+2
         if (R2avg > Ravg || Ravg > Tavg) continue;
         if ((Ravg - R2avg) < 0.2 * (Tavg - Ravg)) break;                  //  0.1 --> 0.2
      }
      
      sumx = sumy = sumr = 0;
      rad = int(1.2 * rad + 1);
      if (rad > radlim) rad = radlim;
      
      for (py = cy-rad; py <= cy+rad; py++)                                //  compute center of gravity for
      for (px = cx-rad; px <= cx+rad; px++)                                //   pixels within rad of (cx,cy)
      {
         rd = sqrt((px-cx)*(px-cx) + (py-cy)*(py-cy));
         if (rd > rad + 0.5) continue;
         ppix = PXMpix(E3pxm16,px,py);
         redpart = pixred(ppix);                                           //  weight by redness
         sumx += redpart * (px - cx);
         sumy += redpart * (py - cy);
         sumr += redpart;
      }

      rcx = cx + 1.0 * sumx / sumr;                                        //  new center of red-eye
      rcy = cy + 1.0 * sumy / sumr;
      if (fabs(cx0 - rcx) > 0.6 * rad) break;                              //  give up if big movement
      if (fabs(cy0 - rcy) > 0.6 * rad) break;
      cx = int(rcx + 0.5);
      cy = int(rcy + 0.5);
      if (cx == cx1 && cy == cy1) break;                                   //  done if no change
   }

   radlim = redeye_radlim(cx,cy);
   if (rad > radlim) rad = radlim;

   ii = Nredmem++;                                                         //  add red-eye to memory
   redmem[ii].type = 'F';
   redmem[ii].cx = cx;
   redmem[ii].cy = cy;
   redmem[ii].rad = rad;
   redmem[ii].clicks = 0;
   redmem[ii].thresh = 0;
   return ii;
}


//  create type R red-eye (drag an ellipse over red-eye area)

int redeye_createR(int cx, int cy, int ww, int hh)
{
   int      rad, radlim;

   Ftoparc = 1;                                                            //  paint ellipse over image
   toparcx = cx - ww;
   toparcy = cy - hh;
   toparcw = 2 * ww;
   toparch = 2 * hh;

   if (ww > hh) rad = ww;
   else rad = hh;
   radlim = redeye_radlim(cx,cy);
   if (rad > radlim) rad = radlim;

   int ii = Nredmem++;                                                     //  add red-eye to memory
   redmem[ii].type = 'R';
   redmem[ii].cx = cx;
   redmem[ii].cy = cy;
   redmem[ii].ww = 2 * ww;
   redmem[ii].hh = 2 * hh;
   redmem[ii].rad = rad;
   redmem[ii].clicks = 0;
   redmem[ii].thresh = 0;
   return ii;
}


//  darken a red-eye and increase click count

void redeye_darken(int ii)
{
   int         cx, cy, ww, hh, px, py, rad, clicks;
   double      rd, thresh, tstep;
   char        type;
   uint16      *ppix;

   type = redmem[ii].type;
   cx = redmem[ii].cx;
   cy = redmem[ii].cy;
   ww = redmem[ii].ww;
   hh = redmem[ii].hh;
   rad = redmem[ii].rad;
   thresh = redmem[ii].thresh;
   tstep = redmem[ii].tstep;
   clicks = redmem[ii].clicks++;
   
   if (thresh == 0)                                                        //  1st click 
   {
      redeye_distr(ii);                                                    //  get pixel redness distribution
      thresh = redmem[ii].thresh;                                          //  initial redness threshhold
      tstep = redmem[ii].tstep;                                            //  redness step size
      Ftoparc = 0;
   }

   tstep = (thresh - tstep) / thresh;                                      //  convert to reduction factor
   thresh = thresh * pow(tstep,clicks);                                    //  reduce threshhold by total clicks

   for (py = cy-rad; py <= cy+rad; py++)                                   //  darken pixels over threshhold
   for (px = cx-rad; px <= cx+rad; px++)
   {
      if (type == 'R') {
         if (px < cx - ww/2) continue;
         if (px > cx + ww/2) continue;
         if (py < cy - hh/2) continue;
         if (py > cy + hh/2) continue;
      }
      rd = sqrt((px-cx)*(px-cx) + (py-cy)*(py-cy));
      if (rd > rad + 0.5) continue;
      ppix = PXMpix(E3pxm16,px,py);                                        //  set redness = threshhold
      if (pixred(ppix) > thresh)
         ppix[0] = int(thresh * (0.65 * ppix[1] + 0.10 * ppix[2] + 1) / (25 - 0.25 * thresh));
   }

   return;
}


//  Build a distribution of redness for a red-eye. Use this information 
//  to set initial threshhold and step size for stepwise darkening.

void redeye_distr(int ii)
{
   int         cx, cy, ww, hh, rad, px, py;
   int         bin, npix, dbins[20], bsum, blim;
   double      rd, maxred, minred, redpart, dbase, dstep;
   char        type;
   uint16      *ppix;
   
   type = redmem[ii].type;
   cx = redmem[ii].cx;
   cy = redmem[ii].cy;
   ww = redmem[ii].ww;
   hh = redmem[ii].hh;
   rad = redmem[ii].rad;
   
   maxred = 0;
   minred = 100;

   for (py = cy-rad; py <= cy+rad; py++)
   for (px = cx-rad; px <= cx+rad; px++)
   {
      if (type == 'R') {
         if (px < cx - ww/2) continue;
         if (px > cx + ww/2) continue;
         if (py < cy - hh/2) continue;
         if (py > cy + hh/2) continue;
      }
      rd = sqrt((px-cx)*(px-cx) + (py-cy)*(py-cy));
      if (rd > rad + 0.5) continue;
      ppix = PXMpix(E3pxm16,px,py);
      redpart = pixred(ppix);
      if (redpart > maxred) maxred = redpart;
      if (redpart < minred) minred = redpart;
   }
   
   dbase = minred;
   dstep = (maxred - minred) / 19.99;

   for (bin = 0; bin < 20; bin++) dbins[bin] = 0;
   npix = 0;

   for (py = cy-rad; py <= cy+rad; py++)
   for (px = cx-rad; px <= cx+rad; px++)
   {
      if (type == 'R') {
         if (px < cx - ww/2) continue;
         if (px > cx + ww/2) continue;
         if (py < cy - hh/2) continue;
         if (py > cy + hh/2) continue;
      }
      rd = sqrt((px-cx)*(px-cx) + (py-cy)*(py-cy));
      if (rd > rad + 0.5) continue;
      ppix = PXMpix(E3pxm16,px,py);
      redpart = pixred(ppix);
      bin = int((redpart - dbase) / dstep);
      ++dbins[bin];
      ++npix;
   }
   
   bsum = 0;
   blim = int(0.5 * npix);

   for (bin = 0; bin < 20; bin++)                                          //  find redness level for 50% of
   {                                                                       //    pixels within red-eye radius
      bsum += dbins[bin];
      if (bsum > blim) break;
   }

   redmem[ii].thresh = dbase + dstep * bin;                                //  initial redness threshhold
   redmem[ii].tstep = dstep;                                               //  redness step (5% of range)

   return;
}


//  find a red-eye (nearly) overlapping the mouse click position

int redeye_find(int cx, int cy)
{
   for (int ii = 0; ii < Nredmem; ii++)
   {
      if (cx > redmem[ii].cx - 2 * redmem[ii].rad && 
          cx < redmem[ii].cx + 2 * redmem[ii].rad &&
          cy > redmem[ii].cy - 2 * redmem[ii].rad && 
          cy < redmem[ii].cy + 2 * redmem[ii].rad) 
            return ii;                                                     //  found
   }
   return -1;                                                              //  not found
}


//  remove a red-eye from memory

void redeye_remove(int ii)
{
   int      cx, cy, rad, px, py;
   uint16   *pix1, *pix3;

   cx = redmem[ii].cx;
   cy = redmem[ii].cy;
   rad = redmem[ii].rad;

   for (px = cx-rad; px <= cx+rad; px++)
   for (py = cy-rad; py <= cy+rad; py++)
   {
      pix1 = PXMpix(E1pxm16,px,py);
      pix3 = PXMpix(E3pxm16,px,py);
      pix3[0] = pix1[0];
      pix3[1] = pix1[1];
      pix3[2] = pix1[2];
   }
   
   for (ii++; ii < Nredmem; ii++) 
      redmem[ii-1] = redmem[ii];
   Nredmem--;
   
   Ftoparc = 0;
   return;
}


//  compute red-eye radius limit: smaller of 100 and nearest image edge

int redeye_radlim(int cx, int cy)
{
   int radlim = 100;
   if (cx < 100) radlim = cx;
   if (E3ww-1 - cx < 100) radlim = E3ww-1 - cx;
   if (cy < 100) radlim = cy;
   if (E3hh-1 - cy < 100) radlim = E3hh-1 - cy;
   return radlim;
}


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

//  image blur function                                                    //  radius steps of 0.5   v.9.2

double      blur_radius;
double      blur_weight[101][101];                                         //  up to blur radius = 99   v.9.2
int         blur_Npixels, blur_pixdone;
int         blur_cancel;

editfunc    EFblur;


void m_blur(GtkWidget *, cchar *)
{
   int    blur_dialog_event(zdialog *zd, cchar *event);
   void * blur_thread(void *);

   zfuncs::F1_help_topic = "blur";                                         //  v.10.8

   EFblur.funcname = "blur";
   EFblur.Farea = 2;                                                       //  select area usable
   EFblur.threadfunc = blur_thread;                                        //  thread function
   if (! edit_setup(EFblur)) return;                                       //  setup edit

   blur_radius = 0.5;
   blur_cancel = 0;

   zdialog *zd = zdialog_new(ZTX("Set Blur Radius"),mWin,Bdone,Bcancel,null);
   EFblur.zd = zd;

   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=10");
   zdialog_add_widget(zd,"label","labrad","hb2",Bradius,"space=5");
   zdialog_add_widget(zd,"spin","radius","hb2","0|99|0.5|0.5","space=5");
   zdialog_add_widget(zd,"button","apply","hb2",Bapply,"space=5");

   zdialog_help(zd,"blur");                                                //  zdialog help topic        v.11.08
   zdialog_run(zd,blur_dialog_event,"save");                               //  run dialog - parallel     v.11.07

   return;
}


//  dialog event and completion callback function

int blur_dialog_event(zdialog * zd, cchar *event)
{
   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(EFblur);                               //  done
      else {
         blur_cancel = 1;                                                  //  v.11.09
         edit_cancel(EFblur);                                              //  cancel or destroy
      }
      return 1;
   }

   if (strEqu(event,"blendwidth")) signal_thread();                        //  v.10.3

   if (strEqu(event,"apply")) {
      zdialog_fetch(zd,"radius",blur_radius);                              //  get blur radius
      if (blur_radius == 0) edit_reset();
      else signal_thread();                                                //  trigger working thread
   }

   return 1;
}


//  image blur thread function

void * blur_thread(void *)
{
   void * blur_wthread(void *arg);

   int         dx, dy;
   double      rad, rad2;
   double      m, d, w, sum;
  
   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request

      rad = blur_radius - 0.2;                                             //  v.9.2
      rad2 = rad * rad;

      for (dx = 0; dx <= rad+1; dx++)                                      //  clear weights array
      for (dy = 0; dy <= rad+1; dy++)
         blur_weight[dx][dy] = 0;

      for (dx = -rad-1; dx <= rad+1; dx++)                                 //  blur_weight[dx][dy] = no. of pixels
      for (dy = -rad-1; dy <= rad+1; dy++)                                 //    at distance (dx,dy) from center
         ++blur_weight[abs(dx)][abs(dy)];

      m = sqrt(rad2 + rad2);                                               //  corner pixel distance from center
      sum = 0;

      for (dx = 0; dx <= rad+1; dx++)                                      //  compute weight of pixel
      for (dy = 0; dy <= rad+1; dy++)                                      //    at distance dx, dy
      {
         d = sqrt(dx*dx + dy*dy);
         w = (m + 1.2 - d) / m;                                            //  v.9.2
         w = w * w;
         sum += blur_weight[dx][dy] * w;
         blur_weight[dx][dy] = w;
      }

      for (dx = 0; dx <= rad+1; dx++)                                      //  make weights add up to 1.0
      for (dy = 0; dy <= rad+1; dy++)
         blur_weight[dx][dy] = blur_weight[dx][dy] / sum;
      
      if (Factivearea) SB_goal = sa_Npixel;
      else  SB_goal = E3ww * E3hh;
      SB_done = 0;                                                         //  v.11.06

      for (int ii = 0; ii < Nwt; ii++)                                     //  start worker threads
         start_wthread(blur_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion

      SB_goal = 0;
      CEF->Fmod = 1;
      mwpaint2();                                                          //  update window
   }

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


void * blur_wthread(void *arg)                                             //  worker thread function
{
   int      index = *((int *) arg);
   int      px, py, ii, dist = 0;
   int      jj, dx, dy, adx, ady, rad;
   double   red, green, blue;
   double   weight1, weight2, f1, f2;
   uint16   *pix1, *pix3, *pixN;
   
   for (py = index; py < E3hh-1; py += Nwt)                                //  loop all image pixels
   for (px = 0; px < E3ww-1; px++)
   {
      if (blur_cancel) exit_wthread();                                     //  v.11.09

      if (Factivearea) {                                                   //  select area active
         ii = py * Fww + px;
         dist = sa_pixmap[ii];                                             //  distance from edge
         if (! dist) continue;                                             //  outside pixel
      }

      pix1 = PXMpix(E1pxm16,px,py);                                        //  source pixel
      pix3 = PXMpix(E3pxm16,px,py);                                        //  target pixel
      
      rad = blur_radius;
      red = green = blue = 0;
      weight2 = 0.0;
      
      if (Factivearea)                                                     //  select area active
      {
         for (dy = -rad-1; dy <= rad+1; dy++)                              //  loop neighbor pixels within radius
         for (dx = -rad-1; dx <= rad+1; dx++)
         {
            if (px+dx < 0 || px+dx > E3ww-1) continue;                     //  omit pixels off edge
            if (py+dy < 0 || py+dy > E3hh-1) continue;
            jj = (py+dy) * E3ww + (px+dx);
            if (! sa_pixmap[jj]) continue;                                 //  omit pixels outside area
            adx = abs(dx);
            ady = abs(dy);
            pixN = pix1 + (dy * E3ww + dx) * 3;
            weight1 = blur_weight[adx][ady];                               //  weight at distance (dx,dy)
            weight2 += weight1;
            red += pixN[0] * weight1;                                      //  accumulate contributions
            green += pixN[1] * weight1;
            blue += pixN[2] * weight1;
         }
         
         red = red / weight2;                                              //  weighted average
         green = green / weight2;
         blue = blue / weight2;

         if (dist < sa_blend) {                                            //  select area is active,
            f1 = 1.0 * dist / sa_blend;                                    //    blend changes over sa_blend
            f2 = 1.0 - f1;
            red = f1 * red + f2 * pix1[0];
            green = f1 * green + f2 * pix1[1];
            blue = f1 * blue + f2 * pix1[2];
         }

         pix3[0] = int(red);
         pix3[1] = int(green);
         pix3[2] = int(blue);
      }

      else
      {
         for (dy = -rad-1; dy <= rad+1; dy++)                              //  loop neighbor pixels within radius
         for (dx = -rad-1; dx <= rad+1; dx++)
         {
            if (px+dx < 0 || px+dx > E3ww-1) continue;                     //  omit pixels off edge
            if (py+dy < 0 || py+dy > E3hh-1) continue;
            adx = abs(dx);
            ady = abs(dy);
            pixN = pix1 + (dy * E3ww + dx) * 3;
            weight1 = blur_weight[adx][ady];                               //  weight at distance (dx,dy)
            weight2 += weight1;
            red += pixN[0] * weight1;                                      //  accumulate contributions
            green += pixN[1] * weight1;
            blue += pixN[2] * weight1;
         }

         red = red / weight2;                                              //  weighted average
         green = green / weight2;
         blue = blue / weight2;

         pix3[0] = red;
         pix3[1] = green;
         pix3[2] = blue;
      }
      
      SB_done++;                                                           //  track progress         v.10.6
   }

   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


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

//  image sharpening function

int      sharp_ED_cycles;
int      sharp_ED_reduce;
int      sharp_ED_thresh;
int      sharp_UM_radius;
int      sharp_UM_amount;
int      sharp_UM_thresh;
int      sharp_UM_Fcalc;
int      sharp_GR_amount;
int      sharp_GR_thresh;
char     sharp_function[4];

editfunc    EFsharp;


void m_sharpen(GtkWidget *, cchar *)
{
   int    sharp_dialog_event(zdialog *zd, cchar *event);
   void * sharp_thread(void *);

   zfuncs::F1_help_topic = "sharpen";                                      //  v.10.8

   EFsharp.funcname = "sharpen";
   EFsharp.Farea = 2;                                                      //  select area usable
   EFsharp.threadfunc = sharp_thread;                                      //  thread function
   if (! edit_setup(EFsharp)) return;                                      //  setup edit

   zdialog *zd = zdialog_new(ZTX("Sharpen Image"),mWin,Bdone,Bcancel,null); 
   EFsharp.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb11","hb1",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb12","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb13","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"button","ED","vb11",ZTX("edge detection"),"space=5");
   zdialog_add_widget(zd,"label","lab11","vb12",ZTX("cycles"));
   zdialog_add_widget(zd,"label","lab12","vb12",ZTX("reduce"));
   zdialog_add_widget(zd,"label","lab13","vb12",Bthresh);
   zdialog_add_widget(zd,"spin","cyclesED","vb13","1|30|1|10");
   zdialog_add_widget(zd,"spin","reduceED","vb13","50|95|1|80");
   zdialog_add_widget(zd,"spin","threshED","vb13","1|99|1|1");

   zdialog_add_widget(zd,"hsep","sep2","dialog");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb21","hb2",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb22","hb2",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb23","hb2",0,"homog|space=5");
   zdialog_add_widget(zd,"button","UM","vb21",ZTX("unsharp mask"),"space=5");
   zdialog_add_widget(zd,"label","lab21","vb22",Bradius);
   zdialog_add_widget(zd,"label","lab22","vb22",Bamount);
   zdialog_add_widget(zd,"label","lab23","vb22",Bthresh);
   zdialog_add_widget(zd,"spin","radiusUM","vb23","1|20|1|2");
   zdialog_add_widget(zd,"spin","amountUM","vb23","0|200|1|100");
   zdialog_add_widget(zd,"spin","threshUM","vb23","0|100|1|0");

   zdialog_add_widget(zd,"hsep","sep3","dialog");
   zdialog_add_widget(zd,"hbox","hb3","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb31","hb3",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb32","hb3",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb33","hb3",0,"homog|space=5");
   zdialog_add_widget(zd,"button","GR","vb31",ZTX("brightness gradient"),"space=5");
   zdialog_add_widget(zd,"label","lab32","vb32",Bamount);
   zdialog_add_widget(zd,"label","lab33","vb32",Bthresh);
   zdialog_add_widget(zd,"spin","amountGR","vb33","0|400|1|100");
   zdialog_add_widget(zd,"spin","threshGR","vb33","0|100|1|0");

   zdialog_help(zd,"sharpen");                                             //  zdialog help topic        v.11.08
   zdialog_run(zd,sharp_dialog_event,"save");                              //  run dialog - parallel     v.11.07

   *sharp_function = 0;
   sharp_UM_Fcalc = 1;
   return;
}


//  dialog event and completion callback function

int sharp_dialog_event(zdialog *zd, cchar *event)
{
   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(EFsharp);
      else edit_cancel(EFsharp);
      return 0;
   }

   if (strEqu(event,"blendwidth")) signal_thread();                        //  v.10.3
   if (strEqu(event,"radiusUM")) sharp_UM_Fcalc = 1;                       //  must recalculate

   if (strcmpv(event,"ED","UM","GR",null))
   {
      edit_reset();                                                        //  restore original image

      zdialog_fetch(zd,"cyclesED",sharp_ED_cycles);                        //  get all input values
      zdialog_fetch(zd,"reduceED",sharp_ED_reduce);
      zdialog_fetch(zd,"threshED",sharp_ED_thresh);
      zdialog_fetch(zd,"radiusUM",sharp_UM_radius);
      zdialog_fetch(zd,"amountUM",sharp_UM_amount);
      zdialog_fetch(zd,"threshUM",sharp_UM_thresh);
      zdialog_fetch(zd,"amountGR",sharp_GR_amount);
      zdialog_fetch(zd,"threshGR",sharp_GR_thresh);

      strcpy(sharp_function,event);                                        //  pass to working thread
      signal_thread();
   }

   return 0;
}


//  sharpen image thread function

void * sharp_thread(void *)
{
   int    sharp_ED();
   int    sharp_UM();
   int    sharp_GR();
   
   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request

      if (strEqu(sharp_function,"ED")) sharp_ED();                         //  do requested function
      if (strEqu(sharp_function,"UM")) sharp_UM();
      if (strEqu(sharp_function,"GR")) sharp_GR();

      CEF->Fmod = 1;
      mwpaint2(); 
   }

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


//  image sharpen function by edge detection and compression

int sharp_ED()
{
   void  sharp_pixel_ED(int px, int py, int thresh);

   int      sharp_thresh1 = 100;                                           //  initial threshold
   double   sharp_thresh2 = 0.01 * sharp_ED_reduce;                        //  decline rate
   int      px, py, thresh, cycles;
   
   thresh = sharp_thresh1;
    
   if (Factivearea) SB_goal = sa_Npixel;                                   //  v.9.6
   else  SB_goal = E3ww * E3hh;
   SB_goal *= sharp_ED_cycles;
   SB_done = 0;                                                            //  v.11.06

   for (cycles = 0; cycles < sharp_ED_cycles; cycles++)
   {
      if (cycles > 0) thresh = int(thresh * sharp_thresh2);

      for (py = 2; py < E3hh-2; py++)
      for (px = 2; px < E3ww-2; px++)                                      //  loop all pixels
      {
         sharp_pixel_ED(px,py,thresh);
         SB_done++;                                                        //  track progress         v.10.6
      }
   }

   SB_goal = 0;
   return 1;
}


void sharp_pixel_ED(int px, int py, int thresh)
{
   uint16   *pix1, *pix1u, *pix1d;
   uint16   *pix3, *pix3u, *pix3d, *pix3uu, *pix3dd;
   int      ii, dist = 0;
   int      dd, rgb, pthresh;
   int      dx[4] = { -1, 0, 1, 1 };                                       //  4 directions: NW N NE E
   int      dy[4] = { -1, -1, -1, 0 };
   int      pv2, pv2u, pv2d, pv2uu, pv2dd, pvdiff;
   double   f1, f2;
   
   if (Factivearea) {                                                      //  select area active
      ii = py * Fww + px;
      dist = sa_pixmap[ii];                                                //  distance from edge
      if (! dist) return;                                                  //  outside pixel
   }

   pthresh = sharp_ED_thresh;                                              //  pthresh = larger
   if (thresh > pthresh) pthresh = thresh;

   pix1 = PXMpix(E1pxm16,px,py);                                           //  input pixel
   pix3 = PXMpix(E3pxm16,px,py);                                           //  output pixel

   for (dd = 0; dd < 4; dd++)                                              //  4 directions
   {
      pix3u = pix3 + (dy[dd] * E3ww + dx[dd]) * 3;                         //  upstream pixel
      pix3d = pix3 - (dy[dd] * E3ww - dx[dd]) * 3;                         //  downstream pixel

      for (rgb = 0; rgb < 3; rgb++)                                        //  loop 3 RGB colors
      {
         pv2 = pix3[rgb];
         pv2u = pix3u[rgb];                                                //  brightness difference
         pv2d = pix3d[rgb];                                                //    across target pixel

         pvdiff = pv2d - pv2u;
         if (pvdiff < 0) pvdiff = -pvdiff;
         if (pvdiff < 256 * pthresh) continue;                             //  brightness slope < threshold

         if (pv2u < pv2 && pv2 < pv2d)                                     //  slope up, monotone
         {
            pix3uu = pix3u + (dy[dd] * E3ww + dx[dd]) * 3;                 //  upstream of upstream pixel
            pix3dd = pix3d - (dy[dd] * E3ww - dx[dd]) * 3;                 //  downstream of downstream
            pv2uu = pix3uu[rgb];
            pv2dd = pix3dd[rgb];

            if (pv2uu >= pv2u) {                                           //  shift focus of changes to
               pix3u = pix3;                                               //    avoid up/down/up jaggies
               pv2u = pv2;
            }
            
            if (pv2dd <= pv2d) {
               pix3d = pix3;
               pv2d = pv2;
            }
               
            if (pv2u > 256) pv2u -= 256;
            if (pv2d < 65279) pv2d += 256;
         }
         
         else if (pv2u > pv2 && pv2 > pv2d)                                //  slope down, monotone
         {
            pix3uu = pix3u + (dy[dd] * E3ww + dx[dd]) * 3;
            pix3dd = pix3d - (dy[dd] * E3ww - dx[dd]) * 3;
            pv2uu = pix3uu[rgb];
            pv2dd = pix3dd[rgb];

            if (pv2uu <= pv2u) {
               pix3u = pix3;
               pv2u = pv2;
            }
            
            if (pv2dd >= pv2d) {
               pix3d = pix3;
               pv2d = pv2;
            }

            if (pv2d > 256) pv2d -= 256;
            if (pv2u < 65279) pv2u += 256;
         }

         else continue;                                                    //  slope too small

         if (Factivearea && dist < sa_blend) {                             //  select area is active,
            f1 = 1.0 * dist / sa_blend;                                    //    blend changes over sa_blend
            f2 = 1.0 - f1;
            pix1u = pix1 + (dy[dd] * E1ww + dx[dd]) * 3;                   //  upstream input pixel
            pix1d = pix1 - (dy[dd] * E1ww - dx[dd]) * 3;                   //  downstream input pixel
            pv2u = int(f1 * pv2u + f2 * pix1u[rgb]);
            pv2d = int(f1 * pv2d + f2 * pix1d[rgb]);
         }

         pix3u[rgb] = pv2u;                                                //  modified brightness values
         pix3d[rgb] = pv2d;                                                //    >> image3 pixel
      }
   }

   return;
}


//  image sharpen function using unsharp mask

int sharp_UM()
{
   void * sharp_UM_wthread(void *arg);
   
   int      ii;
   
   if (sharp_UM_Fcalc) {                                                   //  speedup   v.9.6 
      sharp_UM_Fcalc = 0;
      brhood_calc(sharp_UM_radius,'f');
   }   

   if (Factivearea) SB_goal = sa_Npixel;                                   //  v.9.6
   else  SB_goal = E3ww * E3hh;
   SB_done = 0;                                                            //  v.11.06

   for (ii = 0; ii < Nwt; ii++)                                            //  start worker threads
      start_wthread(sharp_UM_wthread,&wtnx[ii]);
   wait_wthreads();                                                        //  wait for completion

   SB_goal = 0;
   return 1;
}


void * sharp_UM_wthread(void *arg)                                         //  worker thread function
{
   void  sharp_pixel_UM(int px, int py, int index);

   int      index = *((int *) arg);
   int      px, py;
   
   for (py = index; py < E3hh; py += Nwt)                                  //  loop all image3 pixels
   for (px = 0; px < E3ww; px++)
      sharp_pixel_UM(px,py,index);

   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


void sharp_pixel_UM(int px, int py, int index)                             //  process one pixel
{                                                                          //  revised  v.9.6
   int         ii, dist = 0;
   double      amount, thresh, bright;
   double      mean, incr, ratio, f1, f2;
   double      red1, green1, blue1, red3, green3, blue3;
   uint16      *pix1, *pix3;
   
   if (Factivearea) {                                                      //  select area active
      ii = py * Fww + px;
      dist = sa_pixmap[ii];                                                //  distance from edge
      if (! dist) return;                                                  //  outside pixel
   }

   amount = 0.01 * sharp_UM_amount;                                        //  0.0 to 2.0
   thresh = 100 * sharp_UM_thresh;                                         //  0 to 10K (64K max. possible)

   pix1 = PXMpix(E1pxm16,px,py);                                           //  input pixel
   pix3 = PXMpix(E3pxm16,px,py);                                           //  output pixel

   bright = pixbright(pix1);
   if (bright < 100) return;                                               //  effectively black
   mean = get_brhood(px,py);
   incr = (bright - mean);
   if (fabs(incr) < thresh) return;                                        //  omit low-contrast pixels

   incr = incr * amount;                                                   //  0.0 to 2.0
   if (bright + incr > 65535) incr = 65535 - bright;
   ratio = (bright + incr) / bright;
   if (ratio < 0) ratio = 0;                                               //  v.11.08

   red1 = pix1[0];                                                         //  input RGB
   green1 = pix1[1];
   blue1 = pix1[2];
   
   red3 = ratio * red1;                                                    //  output RGB
   if (red3 > 65535) red3 = 65535;
   green3 = ratio * green1;
   if (green3 > 65535) green3 = 65535;
   blue3 = ratio * blue1;
   if (blue3 > 65535) blue3 = 65535;

   if (Factivearea && dist < sa_blend) {                                   //  select area is active,
      f1 = 1.0 * dist / sa_blend;                                          //    blend changes over sa_blend
      f2 = 1.0 - f1;
      red3 = f1 * red3 + f2 * red1;
      green3 = f1 * green3 + f2 * green1;
      blue3 = f1 * blue3 + f2 * blue1;
   }
   
   pix3[0] = red3;
   pix3[1] = green3;
   pix3[2] = blue3;

   SB_done++;                                                              //  track progress         v.10.6
   return;
}


//  sharpen image by increasing brightness gradient                        //  new v.9.8

int sharp_GR()
{
   uint16      *pix1, *pix3;
   int         ii, px, py, dist = 0;   
   double      amount, thresh;
   double      b1, b1x, b1y, b3x, b3y, b3, bf, f1, f2;
   double      red1, green1, blue1, red3, green3, blue3;

   amount = 1 + 0.01 * sharp_GR_amount;                                    //  1.0 - 5.0
   thresh = 655.35 * sharp_GR_thresh;                                      //  0 - 64K

   if (Factivearea) SB_goal = sa_Npixel;
   else  SB_goal = E3ww * E3hh;
   SB_done = 0;                                                            //  v.11.06

   for (py = 1; py < E1hh; py++)                                           //  loop all image pixels
   for (px = 1; px < E1ww; px++)
   {
      if (Factivearea) {                                                   //  select area active
         ii = py * Fww + px;
         dist = sa_pixmap[ii];                                             //  distance from edge
         if (! dist) continue;                                             //  pixel is outside area
      }

      pix1 = PXMpix(E1pxm16,px,py);                                        //  input pixel
      pix3 = PXMpix(E3pxm16,px,py);                                        //  output pixel

      b1 = pixbright(pix1);                                                //  pixel brightness, 0 - 64K
      b1x = b1 - pixbright(pix1 - 3);                                      //  brightness gradient (x,y)
      b1y = b1 - pixbright(pix1 - 3 * E1ww);
      
      if (abs(b1x + b1y) < thresh)                                         //  moderate brightness change for
         f1 = abs(b1x + b1y) / thresh;                                     //    pixels below threshold gradient
      else  f1 = 1.0;                                                      //  v.10.9
      f2 = 1.0 - f1;

      b1x = b1x * amount;                                                  //  amplified gradient
      b1y = b1y * amount;

      b3x = pixbright(pix1 - 3) + b1x;                                     //  + prior pixel brightness
      b3y = pixbright(pix1 - 3 * E3ww) + b1y;                              //  = new brightness
      b3 = 0.5 * (b3x + b3y);

      b3 = f1 * b3 + f2 * b1;                                              //  possibly moderated     v.10.9

      bf = b3 / b1;                                                        //  ratio of brightness change
      if (bf < 0) bf = 0;
      if (bf > 4) bf = 4;
      
      red1 = pix1[0];                                                      //  input RGB
      green1 = pix1[1];
      blue1 = pix1[2];
      
      red3 = bf * red1;                                                    //  output RGB
      if (red3 > 65535) red3 = 65535;
      green3 = bf * green1;
      if (green3 > 65535) green3 = 65535;
      blue3 = bf * blue1;
      if (blue3 > 65535) blue3 = 65535;

      if (Factivearea && dist < sa_blend) {                                //  select area is active,
         f1 = 1.0 * dist / sa_blend;                                       //    blend changes over sa_blend
         f2 = 1.0 - f1;
         red3 = f1 * red3 + f2 * red1;
         green3 = f1 * green3 + f2 * green1;
         blue3 = f1 * blue3 + f2 * blue1;
      }
      
      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;

      SB_done++;                                                           //  track progress         v.10.6
   }

   SB_goal = 0;
   return 1;
}


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

//  image noise reduction

int      denoise_method = 5;                                               //  default algorithm
int      denoise_radius = 4;

editfunc    EFdenoise;


void m_denoise(GtkWidget *, cchar *)
{
   int    denoise_dialog_event(zdialog *zd, cchar *event);                 //  dialog event function
   void * denoise_thread(void *);

   cchar  *denoise_message = ZTX(" Press the reduce button to \n"
                                 " reduce noise in small steps. \n"
                                 " Use undo to start over.");

   zfuncs::F1_help_topic = "reduce_noise";                                 //  v.10.8

   EFdenoise.funcname = "denoise";
   EFdenoise.Farea = 2;                                                    //  select area usable
   EFdenoise.threadfunc = denoise_thread;                                  //  thread function
   if (! edit_setup(EFdenoise)) return;                                    //  setup edit

   zdialog *zd = zdialog_new(ZTX("Noise Reduction"),mWin,Bdone,Bcancel,null);
   EFdenoise.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",denoise_message,"space=5");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labalg","hb1",ZTX("algorithm"),"space=5");
   zdialog_add_widget(zd,"combo","method","hb1",0,"space=5|expand");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labrad","hb2",Bradius,"space=5");
   zdialog_add_widget(zd,"spin","radius","hb2","1|9|1|4","space=5");
   zdialog_add_widget(zd,"button","reduce","hb2",Breduce,"space=5");
   
   zdialog_cb_app(zd,"method",ZTX("flatten outliers by color (1)"));
   zdialog_cb_app(zd,"method",ZTX("flatten outliers by color (2)"));
   zdialog_cb_app(zd,"method",ZTX("set median brightness by color"));
   zdialog_cb_app(zd,"method",ZTX("top hat filter by color"));
   zdialog_stuff(zd,"method",ZTX("top hat filter by color"));              //  default

   zdialog_help(zd,"reduce_noise");                                        //  zdialog help topic        v.11.08
   zdialog_run(zd,denoise_dialog_event,"save");                            //  run dialog - parallel     v.11.07
   return;
}


//  dialog event and completion callback function

int denoise_dialog_event(zdialog * zd, cchar *event)
{
   char     method[40];

   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(EFdenoise);
      else edit_cancel(EFdenoise);
      return 0;
   }

   if (strEqu(event,"blendwidth")) signal_thread();                        //  trigger update thread

   if (strEqu(event,"radius")) 
      zdialog_fetch(zd,"radius",denoise_radius);

   if (strEqu(event,"method")) 
   {
      zdialog_fetch(zd,"method",method,39);

      if (strEqu(method,"flatten outliers by color (1)")) {
         denoise_method = 1;
         denoise_radius = 1;
      }

      if (strEqu(method,"flatten outliers by color (2)")) {
         denoise_method = 2;
         denoise_radius = 3;
      }

      if (strEqu(method,"set median brightness by color")) {
         denoise_method = 4;
         denoise_radius = 2;
      }

      if (strEqu(method,"top hat filter by color")) {
         denoise_method = 5;
         denoise_radius = 4;
      }
      
      zdialog_stuff(zd,"radius",denoise_radius);
   }
   
   if (strEqu(event,"reduce")) signal_thread();                            //  trigger update thread

   return 1;
}


//  image noise reduction thread

void * denoise_thread(void *)
{
   void * denoise_wthread(void *arg);
   
   int      ii;

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

      E9pxm16 = PXM_copy(E3pxm16);                                         //  image3 is reference source
                                                                           //  image9 will be modified
      if (Factivearea) SB_goal = sa_Npixel;
      else  SB_goal = E3ww * E3hh;
      SB_done = 0;                                                         //  v.11.06

      for (ii = 0; ii < Nwt; ii++)                                         //  start worker threads
         start_wthread(denoise_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion

      SB_goal = 0;

      mutex_lock(&Fpixmap_lock);
      PXM_free(E3pxm16);                                                   //  image9 >> image3
      E3pxm16 = E9pxm16;
      E9pxm16 = 0;
      mutex_unlock(&Fpixmap_lock);

      CEF->Fmod = 1;
      mwpaint2();                                                          //  update window
   }

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


void * denoise_wthread(void *arg)                                          //  worker thread function
{
   void  denoise_func1(uint16 *pix3, uint16 *pix9);
   void  denoise_func2(uint16 *pix3, uint16 *pix9);
   void  denoise_func4(uint16 *pix3, uint16 *pix9);
   void  denoise_func5(uint16 *pix3, uint16 *pix9);
   
   int         index = *((int *) arg);
   int         ii, px, py, rad, dist = 0;
   double      f1, f2;
   uint16      *pix1, *pix3, *pix9;

   rad = denoise_radius;

   for (py = index+rad; py < E3hh-rad; py += Nwt)                          //  loop all image3 pixels
   for (px = rad; px < E3ww-rad; px++)
   {
      if (Factivearea) {                                                   //  select area active
         ii = py * Fww + px;
         dist = sa_pixmap[ii];                                             //  distance from edge
         if (! dist) continue;                                             //  outside pixel
      }

      pix3 = PXMpix(E3pxm16,px,py);                                        //  source pixel
      pix9 = PXMpix(E9pxm16,px,py);                                        //  target pixel

      if (denoise_method == 1) denoise_func1(pix3,pix9);
      if (denoise_method == 2) denoise_func2(pix3,pix9);
      if (denoise_method == 4) denoise_func4(pix3,pix9);
      if (denoise_method == 5) denoise_func5(pix3,pix9);

      if (Factivearea && dist < sa_blend) {                                //  select area is active,
         f1 = 1.0 * dist / sa_blend;                                       //    blend changes over sa_blend
         f2 = 1.0 - f1;
         pix1 = PXMpix(E1pxm16,px,py);                                     //  source pixel
         pix9[0] = int(f1 * pix9[0] + f2 * pix1[0]);
         pix9[1] = int(f1 * pix9[1] + f2 * pix1[1]);
         pix9[2] = int(f1 * pix9[2] + f2 * pix1[2]);
      }

      SB_done++;                                                           //  track progress         v.10.6
   }

   exit_wthread();
   return 0;                                                               //  not executed, avoid gcc warning
}


//  flatten outliers within radius, by color 
//  an outlier is the max or min value within a radius

void denoise_func1(uint16 *pix3, uint16 *pix9)
{
   int         dy, dx, rad;
   int         min0, min1, min2, max0, max1, max2;
   uint16      *pixN;

   min0 = min1 = min2 = 65535;
   max0 = max1 = max2 = 0;
   rad = denoise_radius;

   for (dy = -rad; dy <= rad; dy++)                                        //  loop surrounding pixels
   for (dx = -rad; dx <= rad; dx++)
   {
      if (dy == 0 && dx == 0) continue;                                    //  skip self

      pixN = pix3 + (dy * E3ww + dx) * 3;
      if (pixN[0] < min0) min0 = pixN[0];                                  //  find min and max per color
      if (pixN[0] > max0) max0 = pixN[0];
      if (pixN[1] < min1) min1 = pixN[1];
      if (pixN[1] > max1) max1 = pixN[1];
      if (pixN[2] < min2) min2 = pixN[2];
      if (pixN[2] > max2) max2 = pixN[2];
   }
   
   if (pix3[0] <= min0 && min0 < 65279) pix9[0] = min0 + 256;              //  if outlier, flatten a little
   if (pix3[0] >= max0 && max0 > 256) pix9[0] = max0 - 256;
   if (pix3[1] <= min1 && min1 < 65279) pix9[1] = min1 + 256;
   if (pix3[1] >= max1 && max1 > 256) pix9[1] = max1 - 256;
   if (pix3[2] <= min2 && min2 < 65279) pix9[2] = min2 + 256;
   if (pix3[2] >= max2 && max2 > 256) pix9[2] = max2 - 256;
   
   return;
}


//  flatten outliers
//  An outlier pixel has an RGB value outside one sigma of 
//  the mean for all pixels within a given radius of the pixel.

void denoise_func2(uint16 *pix3, uint16 *pix9)
{
   int         rgb, dy, dx, rad, nn;
   double      nn1, val, sum, sum2, mean, variance, sigma;
   uint16      *pixN;

   rad = denoise_radius;
   nn = (rad * 2 + 1);
   nn = nn * nn - 1;
   nn1 = 1.0 / nn;

   for (rgb = 0; rgb < 3; rgb++)                                           //  loop RGB color
   {
      sum = sum2 = 0;

      for (dy = -rad; dy <= rad; dy++)                                     //  loop surrounding pixels
      for (dx = -rad; dx <= rad; dx++)
      {
         if (dy == 0 && dx == 0) continue;                                 //  skip self
         pixN = pix3 + (dy * E3ww + dx) * 3;
         val = pixN[rgb];
         sum += val;
         sum2 += val * val;
      }
      
      mean = nn1 * sum;
      variance = nn1 * (sum2 - 2.0 * mean * sum) + mean * mean;
      sigma = sqrt(variance);

      val = pix3[rgb];      
      if (val > mean + sigma) {                                            //  move value to mean +/- sigma
         val = mean + sigma;
         pix9[rgb] = val;
      }
      else if (val < mean - sigma) {
         val = mean - sigma;
         pix9[rgb] = val;
      }
   }
   
   return;
}


//  use median brightness for pixels within radius

void denoise_func4(uint16 *pix3, uint16 *pix9)
{
   int         dy, dx, rad;
   int         ns, rgb, bsortN[400];
   uint16      *pixN;

   rad = denoise_radius;

   for (rgb = 0; rgb < 3; rgb++)                                           //  loop all RGB colors
   {
      ns = 0;

      for (dy = -rad; dy <= rad; dy++)                                     //  loop surrounding pixels
      for (dx = -rad; dx <= rad; dx++)                                     //  get brightness values
      {
         pixN = pix3 + (dy * E3ww + dx) * 3;
         bsortN[ns] = pixN[rgb];
         ns++;
      }

      HeapSort(bsortN,ns);
      pix9[rgb] = bsortN[ns/2];                                            //  median brightness of ns pixels
   }

   return;
}


//  modified top hat filter: execute with increasing radius from 1 to limit
//  detect outlier by comparing with pixels in outer radius

void denoise_func5(uint16 *pix3, uint16 *pix9)
{
   int         dy, dx, rad;
   int         min0, min1, min2, max0, max1, max2;
   uint16      *pixN;

   for (rad = 1; rad <= denoise_radius; rad++)
   for (int loops = 0; loops < 2; loops++)
   {
      min0 = min1 = min2 = 65535;
      max0 = max1 = max2 = 0;

      for (dy = -rad; dy <= rad; dy++)                                     //  loop all pixels within rad
      for (dx = -rad; dx <= rad; dx++)
      {
         if (dx > -rad && dx < rad) continue;                              //  skip inner pixels
         if (dy > -rad && dy < rad) continue;

         pixN = pix3 + (dy * E3ww + dx) * 3;
         if (pixN[0] < min0) min0 = pixN[0];                               //  find min and max per color
         if (pixN[0] > max0) max0 = pixN[0];                               //    among outermost pixels
         if (pixN[1] < min1) min1 = pixN[1];
         if (pixN[1] > max1) max1 = pixN[1];
         if (pixN[2] < min2) min2 = pixN[2];
         if (pixN[2] > max2) max2 = pixN[2];
      }
      
      if (pix3[0] < min0 && pix9[0] < 65279) pix9[0] += 256;               //  if central pixel is outlier,
      if (pix3[0] > max0 && pix9[0] > 256) pix9[0] -= 256;                 //    moderate its values
      if (pix3[1] < min1 && pix9[1] < 65279) pix9[1] += 256;
      if (pix3[1] > max1 && pix9[1] > 256) pix9[1] -= 256;
      if (pix3[2] < min2 && pix9[2] < 65279) pix9[2] += 256;
      if (pix3[2] > max2 && pix9[2] > 256) pix9[2] -= 256;
   }

   return;
}


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

//  Smart Erase menu function - Replace pixels inside a select area 
//    with a reflection of pixels outside the area.

editfunc    EFerase;

void m_smart_erase(GtkWidget *, const char *)                              //  overhauled    v.11.04
{
   int    smart_erase_dialog_event(zdialog* zd, const char *event);

   int      cc;
   cchar    *erase_message = ZTX("1. Drag mouse to select. \n"
                                 "2. Erase.   3. Repeat. ");

   zfuncs::F1_help_topic = "smart_erase";
   
   EFerase.funcname = "smart-erase";
   EFerase.Farea = 0;                                                      //  select area deleted
   EFerase.mousefunc = sa_mouse_mousefunc;                                 //  mouse function (use select area)  v.11.12
   if (! edit_setup(EFerase)) return;                                      //  setup edit
   
/*     ______________________________
      |                              |
      | 1. Drag mouse to select.     |
      | 2. Erase.   3. Repeat.       |
      |                              |
      | Radius [10|v]  Blur [0.5|v]  |
      | [New Area]  [Erase]  [Undo]  |
      |                              |
      |                    [Done]    |
      |______________________________|
*/

   zdialog *zd = zdialog_new(ZTX("Smart Erase"),mWin,Bdone,null);
   EFerase.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",erase_message,"space=8");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labr","hb2",ZTX("Radius"),"space=5");
   zdialog_add_widget(zd,"spin","radius","hb2","1|20|1|5");
   zdialog_add_widget(zd,"label","labb","hb2",ZTX("Blur"),"space=5");
   zdialog_add_widget(zd,"spin","blur","hb2","0|9|0.5|0.5");
   zdialog_add_widget(zd,"hbox","hb3","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","newarea","hb3",ZTX("New Area"),"space=10");
   zdialog_add_widget(zd,"button","erase","hb3",Berase,"space=10");
   zdialog_add_widget(zd,"button","undo1","hb3",Bundo,"space=5");

   zdialog_help(zd,"smart_erase");                                         //  zdialog help topic        v.11.08
   zdialog_run(zd,smart_erase_dialog_event,"save");                        //  run dialog - parallel     v.11.07

   sa_unselect();                                                          //  unselect area if any        v.11.06.1
   cc = Fww * Fhh * sizeof(uint16);                                        //  create new area
   sa_pixmap = (uint16 *) zmalloc(cc,"smart_erase");
   memset(sa_pixmap,0,cc);
   sa_mode = 5;                                                            //  mode = select by mouse
   sa_stat = 1;                                                            //  status = active edit
   sa_fww = Fww;                                                           //  v.11.08
   sa_fhh = Fhh;
   sa_matchcolor = 0;                                                      //  no color matching            v.11.12
   sa_searchrange = 1;                                                     //  search within mouse radius   v.11.12
   sa_show(1);

   sa_mouseradius = 5;                                                     //  initial mouse select radius
   return;
}


//  dialog event and completion function

int smart_erase_dialog_event(zdialog *zd, const char *event)               //  overhauled    v.11.04
{
   void smart_erase_func(int mode);
   void smart_erase_blur(double radius);
   
   double      radius;
   int         cc;
   
   if (zd->zstat) {
      sa_unselect();
      if (zd->zstat == 1) edit_done(EFerase);
      else edit_cancel(EFerase);
      return 0;
   }
   
   if (strEqu(event,"focus")) {                                            //  toggle mouse capture
      sa_stat = 1;                                                         //  status = active edit
      takeMouse(zd,sa_mouse_mousefunc,0);                                  //  use select area by mouse function
      sa_show(1);
   }
   
   if (strEqu(event,"newarea")) {
      sa_unselect();
      cc = Fww * Fhh * sizeof(uint16);                                     //  create new area
      sa_pixmap = (uint16 *) zmalloc(cc,"smart_erase");
      memset(sa_pixmap,0,cc);
      sa_mode = 5;                                                         //  mode = select by mouse
      sa_stat = 1;                                                         //  status = active edit
      sa_fww = Fww;                                                        //  v.11.08
      sa_fhh = Fhh;
      sa_show(1);
   }
   
   if (strEqu(event,"radius"))
      zdialog_fetch(zd,"radius",sa_mouseradius);

   if (strEqu(event,"erase")) {                                            //  do smart erase
      sa_finish_auto();                                                    //  finish the area
      smart_erase_func(1);
      zdialog_fetch(zd,"blur",radius);                                     //  add optional blur
      if (radius > 0) smart_erase_blur(radius);
      sa_show(0);
      freeMouse();                                                         //  disconnect mouse
   }
   
   if (strEqu(event,"undo1"))                                              //  dialog undo, undo last erase
      smart_erase_func(2);

   return 0;
}


//  erase the area or restore last erased area

void smart_erase_func(int mode)
{
   int         px, py, npx, npy;
   int         qx, qy, sx, sy, tx, ty;
   int         ii, rad, inc, cc;
   int         dist2, mindist2;
   double      slope;
   char        *pmap;
   uint16      *pix1, *pix3;

   if (! Factivearea) return;                                              //  nothing selected       v.11.05
   
   for (py = sa_miny; py < sa_maxy; py++)                                  //  loop all pixels in area
   for (px = sa_minx; px < sa_maxx; px++)
   {
      ii = py * Fww + px;
      if (! sa_pixmap[ii]) continue;                                       //  pixel not selected

      pix1 = PXMpix(E1pxm16,px,py);                                        //  input pixel
      pix3 = PXMpix(E3pxm16,px,py);                                        //  output pixel

      pix3[0] = pix1[0];                                                   //  restore pixels inside area
      pix3[1] = pix1[1];
      pix3[2] = pix1[2];
   }

   mwpaint2();                                                             //  update window
   
   if (mode == 2) return;                                                  //  mode = undo, done

   cc = Fww * Fhh;                                                         //  allocate pixel done map
   pmap = (char *) zmalloc(cc,"smart_erase");
   memset(pmap,0,cc);

   for (py = sa_miny; py < sa_maxy; py++)                                  //  loop all pixels in area
   for (px = sa_minx; px < sa_maxx; px++)
   {
      ii = py * Fww + px;
      if (! sa_pixmap[ii]) continue;                                       //  pixel not selected
      if (pmap[ii]) continue;                                              //  pixel already done

      mindist2 = 999999;                                                   //  find nearest edge
      npx = npy = 0;

      for (rad = 1; rad < 50; rad++)                                       //  50 pixel limit            v.11.05
      {
         for (qx = px-rad; qx <= px+rad; qx++)                             //  search within rad         v.11.05
         for (qy = py-rad; qy <= py+rad; qy++)
         {
            if (qx < 0 || qx >= Fww) continue;                             //  off image edge      modified  v.11.09
            if (qy < 0 || qy >= Fhh) continue;
            ii = qy * Fww + qx;
            if (sa_pixmap[ii]) continue;                                   //  within selected area

            dist2 = (px-qx) * (px-qx) + (py-qy) * (py-qy);                 //  distance**2 to edge pixel
            if (dist2 < mindist2) {
               mindist2 = dist2;
               npx = qx;                                                   //  save nearest edge pixel found
               npy = qy;
            }
         }

         if (rad * rad >= mindist2) break;                                 //  can quit now
      }
      
      if (! npx && ! npy) continue;                                        //  edge not found, should not happen

      qx = npx;                                                            //  nearest edge pixel
      qy = npy;
      
      if (abs(qy - py) > abs(qx - px)) {                                   //  qx/qy = near edge from px/py
         slope = 1.0 * (qx - px) / (qy - py);
         if (qy > py) inc = 1;
         else inc = -1;
         for (sy = py; sy != qy; sy += inc)                                //  line from px/py to qx/qy     v.11.06
         {
            sx = px + slope * (sy - py);
            ii = sy * Fww + sx;
            if (pmap[ii]) continue;                                        //  v.11.06
            pmap[ii] = 1;
            tx = qx + (qx - sx);                                           //  tx/ty = parallel line from qx/qy 
            ty = qy + (qy - sy);                                           //  modified  v.11.09
            if (tx < 0) tx = 0;
            if (tx > Fww-1) tx = Fww-1;
            if (ty < 0) ty = 0;
            if (ty > Fhh-1) ty = Fhh-1;
            pix1 = PXMpix(E3pxm16,tx,ty);                                  //  copy pixel from tx/ty to sx/sy
            pix3 = PXMpix(E3pxm16,sx,sy);
            pix3[0] = pix1[0];
            pix3[1] = pix1[1];
            pix3[2] = pix1[2];
         }
      }

      else {
         slope = 1.0 * (qy - py) / (qx - px);
         if (qx > px) inc = 1;
         else inc = -1;
         for (sx = px; sx != qx; sx += inc)
         {
            sy = py + slope * (sx - px);
            ii = sy * Fww + sx;
            if (pmap[ii]) continue;
            pmap[ii] = 1;
            tx = qx + (qx - sx);
            ty = qy + (qy - sy);
            if (tx < 0) tx = 0;
            if (tx > Fww-1) tx = Fww-1;
            if (ty < 0) ty = 0;
            if (ty > Fhh-1) ty = Fhh-1;
            pix1 = PXMpix(E3pxm16,tx,ty);
            pix3 = PXMpix(E3pxm16,sx,sy);
            pix3[0] = pix1[0];
            pix3[1] = pix1[1];
            pix3[2] = pix1[2];
         }
      }
   }

   zfree(pmap);                                                            //  free memory
   CEF->Fmod = 1;
   mwpaint2();                                                             //  update window
   return;
}


//  add blur to the erased area to help mask the side-effects

int smart_erase_blur(double radius)
{
   int         ii, px, py, dx, dy, adx, ady;
   double      blur_weight[10][10];                                        //  up to blur radius = 9
   double      rad, rad2;
   double      m, d, w, sum;
   double      red, green, blue;
   double      weight1, weight2;
   uint16      *pix9, *pix3, *pixN;
   
   if (! Factivearea) return 0;
  
   rad = radius - 0.2;
   rad2 = rad * rad;

   for (dx = 0; dx <= rad+1; dx++)                                         //  clear weights array
   for (dy = 0; dy <= rad+1; dy++)
      blur_weight[dx][dy] = 0;

   for (dx = -rad-1; dx <= rad+1; dx++)                                    //  blur_weight[dx][dy] = no. of pixels
   for (dy = -rad-1; dy <= rad+1; dy++)                                    //    at distance (dx,dy) from center
      ++blur_weight[abs(dx)][abs(dy)];

   m = sqrt(rad2 + rad2);                                                  //  corner pixel distance from center
   sum = 0;

   for (dx = 0; dx <= rad+1; dx++)                                         //  compute weight of pixel
   for (dy = 0; dy <= rad+1; dy++)                                         //    at distance dx, dy
   {
      d = sqrt(dx*dx + dy*dy);
      w = (m + 1.2 - d) / m;
      w = w * w;
      sum += blur_weight[dx][dy] * w;
      blur_weight[dx][dy] = w;
   }

   for (dx = 0; dx <= rad+1; dx++)                                         //  make weights add up to 1.0
   for (dy = 0; dy <= rad+1; dy++)
      blur_weight[dx][dy] = blur_weight[dx][dy] / sum;
   
   E9pxm16 = PXM_copy(E3pxm16);                                            //  copy edited image
   
   for (py = sa_miny; py < sa_maxy; py++)                                  //  loop all pixels in area
   for (px = sa_minx; px < sa_maxx; px++)
   {
      ii = py * Fww + px;
      if (! sa_pixmap[ii]) continue;                                       //  pixel not in area

      pix9 = PXMpix(E9pxm16,px,py);                                        //  source pixel
      pix3 = PXMpix(E3pxm16,px,py);                                        //  target pixel
      
      rad = radius;
      red = green = blue = 0;
      weight2 = 0.0;
   
      for (dy = -rad-1; dy <= rad+1; dy++)                                 //  loop neighbor pixels within radius
      for (dx = -rad-1; dx <= rad+1; dx++)
      {
         if (px+dx < 0 || px+dx >= E3ww) continue;                         //  omit pixels off edge
         if (py+dy < 0 || py+dy >= E3hh) continue;
         adx = abs(dx);
         ady = abs(dy);
         pixN = pix9 + (dy * E3ww + dx) * 3;
         weight1 = blur_weight[adx][ady];                                  //  weight at distance (dx,dy)
         weight2 += weight1;
         red += pixN[0] * weight1;                                         //  accumulate contributions
         green += pixN[1] * weight1;
         blue += pixN[2] * weight1;
      }
      
      red = red / weight2;                                                 //  weighted average
      green = green / weight2;
      blue = blue / weight2;

      pix3[0] = int(red);
      pix3[1] = int(green);
      pix3[2] = int(blue);
   }

   PXM_free(E9pxm16);

   CEF->Fmod = 1;
   mwpaint2();                                                             //  update window
   return 0;
}


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

//  find and remove "dust" from an image (e.g. from a scanned dusty slide)
//  dust is defined as small dark areas surrounded by brighter areas
//  image 1   original with prior edits
//  image 3   accumulated dust removals that have been comitted
//  image 9   comitted dust removals + pending removal (work in process)

namespace dust_names 
{
   editfunc    EFdust;

   int         spotspann;                                                  //  max. dustspot spann, pixels
   int         spotspann2;                                                 //  spotspann **2
   double      brightness;                                                 //  brightness limit, 0 to 1 = white
   double      contrast;                                                   //  min. contrast, 0 to 1 = black/white
   int         *pixgroup;                                                  //  maps (px,py) to pixel group no.
   int         Fred;                                                       //  red pixels are on

   int         Nstack;

   struct spixstack {
      uint16      px, py;                                                  //  pixel group search stack
      uint16      direc;
   }           *pixstack;

   #define maxgroups 1000000
   int         Ngroups;
   int         groupcount[maxgroups];                                      //  count of pixels in each group
   double      groupbright[maxgroups];                                     //  
   int         edgecount[maxgroups];                                       //  group edge pixel count
   double      edgebright[maxgroups];                                      //  group edge pixel brightness sum

   typedef struct {
      uint16      px1, py1, px2, py2;                                      //  pixel group extreme pixels
      int         spann2;                                                  //  spann from px1/py1 to px2/py2
   }  sgroupspann;
   
   sgroupspann    groupspann[maxgroups];
}


void m_dust(GtkWidget *, const char *)                                     //  new v.11.05
{
   using namespace dust_names;

   int    dust_dialog_event(zdialog *zd, cchar *event);
   void * dust_thread(void *);
   
   zfuncs::F1_help_topic = "remove_dust";                                  //  v.11.05.1

   EFdust.funcname = "dust";
   EFdust.Farea = 2;                                                       //  select area usable
   EFdust.threadfunc = dust_thread;                                        //  thread function
   if (! edit_setup(EFdust)) return;                                       //  setup edit

   E9pxm16 = PXM_copy(E3pxm16);                                            //  image 9 = copy of image3
   Fred = 0;

   int cc = Fww * Fhh * sizeof(int);
   pixgroup = (int *) zmalloc(cc,"erase_dust");                            //  maps pixels to assigned groups

   cc = Fww * Fhh * sizeof(spixstack);
   pixstack = (spixstack *) zmalloc(cc,"erase_dust");                      //  pixel group search stack
   
/***
                     Remove Dust

        spot size limit    =========[]===========
        max. brightness    =============[]=======
        min. contrast      ========[]============
        [erase] [red] [undo last] [apply]

                                  [Done] [Cancel]
***/

   zdialog *zd = zdialog_new(ZTX("Remove Dust"),mWin,Bdone,Bcancel,null);
   EFdust.zd = zd;

   zdialog_add_widget(zd,"hbox","hbssl","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labssl","hbssl",ZTX("spot size limit"),"space=5");
   zdialog_add_widget(zd,"hscale","spotspann","hbssl","1|50|1|20","space=5|expand");
   zdialog_add_widget(zd,"hbox","hbmb","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labmb","hbmb",ZTX("max. brightness"),"space=5");
   zdialog_add_widget(zd,"hscale","brightness","hbmb","1|999|1|700","space=5|expand");
   zdialog_add_widget(zd,"hbox","hbmc","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labmb","hbmc",ZTX("min. contrast"),"space=5");
   zdialog_add_widget(zd,"hscale","contrast","hbmc","1|500|1|50","space=5|expand");
   zdialog_add_widget(zd,"hbox","hbbutts","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","erase","hbbutts",Berase,"space=5");
   zdialog_add_widget(zd,"button","red","hbbutts",Bred,"space=5");
   zdialog_add_widget(zd,"button","undo1","hbbutts",Bundolast,"space=5");
   zdialog_add_widget(zd,"button","apply","hbbutts",Bapply,"space=5");

   zdialog_fetch(zd,"spotspann",spotspann);                                //  max. dustspot spann (pixels)
   spotspann2 = spotspann * spotspann;

   zdialog_fetch(zd,"brightness",brightness);                              //  max. dustspot brightness
   brightness = 0.001 * brightness;                                        //  scale 0 to 1 = white

   zdialog_fetch(zd,"contrast",contrast);                                  //  min. dustspot contrast
   contrast = 0.001 * contrast;                                            //  scale 0 to 1 = black/white

   zdialog_resize(zd,300,0);
   zdialog_help(zd,"remove_dust");                                         //  zdialog help topic        v.11.08
   zdialog_run(zd,dust_dialog_event,"save");                               //  run dialog - parallel     v.11.07

   signal_thread();
   return;
}


//  dialog event and completion callback function

int dust_dialog_event(zdialog *zd, cchar *event)
{
   using namespace dust_names;
   
   void dust_erase();

   if (zd->zstat)                                                          //  dialog complete
   {
      if (zd->zstat == 1) {                                                //  done, use committed changes
         mutex_lock(&Fpixmap_lock);
         PXM_free(E3pxm16);
         E3pxm16 = E9pxm16;                                                //  image 3 = image 9
         E9pxm16 = 0;
         mutex_unlock(&Fpixmap_lock);
         edit_done(EFdust);
      }
      else {                                                               //  cancel, discard changes
         PXM_free(E9pxm16);
         edit_cancel(EFdust);
      }

      zfree(pixgroup);                                                     //  free memory
      zfree(pixstack);
      return 0;
   }
   
   if (strEqu(event,"spotspann") || strEqu(event,"brightness") 
          || strEqu(event,"contrast") || strEqu(event,"red"))
   {
      zdialog_fetch(zd,"spotspann",spotspann);                             //  max. dustspot spann (pixels)
      spotspann2 = spotspann * spotspann;

      zdialog_fetch(zd,"brightness",brightness);                           //  max. dustspot brightness
      brightness = 0.001 * brightness;                                     //  scale 0 to 1 = white
      
      zdialog_fetch(zd,"contrast",contrast);                               //  min. dustspot contrast
      contrast = 0.001 * contrast;                                         //  scale 0 to 1 = black/white

      signal_thread();                                                     //  do the work
   }
   
   if (strEqu(event,"erase")) dust_erase();
   if (strEqu(event,"blendwidth")) dust_erase();
   
   if (strEqu(event,"undo1")) {
      mutex_lock(&Fpixmap_lock);                                           //  image 3 = copy of image 9
      PXM_free(E3pxm16);
      E3pxm16 = PXM_copy(E9pxm16);
      mutex_unlock(&Fpixmap_lock);
      Fred = 0;
      mwpaint2();
   }
   
   if (strEqu(event,"apply")) {
      if (Fred) dust_erase();
      PXM_free(E9pxm16);                                                   //  image 9 = copy of image 3
      E9pxm16 = PXM_copy(E3pxm16);
      CEF->Fmod = 1;
   }
   
   return 0;
}


//  dust find thread function - find the dust particles and mark them

void * dust_thread(void *)
{
   using namespace dust_names;

   int         xspann, yspann, spann2;
   int         group, cc, ii, kk, Nremoved;
   int         px, py, dx, dy, ppx, ppy, npx, npy;
   double      gbright, pbright, pcontrast;
   double      ff = 1.0 / 65536.0;
   uint16      direc, *pix3;
   
   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request
      
      mutex_lock(&Fpixmap_lock);                                           //  image 3 = copy of image 9
      PXM_free(E3pxm16);
      E3pxm16 = PXM_copy(E9pxm16);
      mutex_unlock(&Fpixmap_lock);
      mwpaint2();

      cc = Fww * Fhh * sizeof(int);                                        //  clear group arrays
      memset(pixgroup,0,cc);
      cc = maxgroups * sizeof(int);
      memset(groupcount,0,cc);
      memset(edgecount,0,cc);
      cc = maxgroups * sizeof(double);
      memset(groupbright,0,cc);
      memset(edgebright,0,cc);
      cc = maxgroups * sizeof(sgroupspann);
      memset(groupspann,0,cc);

      group = 0;
      
      for (py = 0; py < Fhh; py++)                                         //  loop all pixels
      for (px = 0; px < Fww; px++)
      {
         ii = py * Fww + px;
         if (Factivearea && ! sa_pixmap[ii]) continue;                     //  not in active area
         if (pixgroup[ii]) continue;                                       //  already assigned to a group

         pix3 = PXMpix(E3pxm16,px,py);                                     //  get pixel brightness
         gbright = ff * pixbright(pix3);                                   //  0 to 1.0 = white
         if (gbright > brightness) continue;                               //  ignore bright pixel

         if (group == maxgroups-1) break;                                  //  too many groups, make no more

         pixgroup[ii] = ++group;                                           //  assign next group
         groupcount[group] = 1;
         groupbright[group] = gbright;

         pixstack[0].px = px;                                              //  put pixel into stack with
         pixstack[0].py = py;                                              //    direction = ahead
         pixstack[0].direc = 0;
         Nstack = 1;

         while (Nstack)
         {
            kk = Nstack - 1;                                               //  get last pixel in stack
            px = pixstack[kk].px;
            py = pixstack[kk].py;
            direc = pixstack[kk].direc;                                    //  next search direction
            
            if (direc == 'x') {
               Nstack--;                                                   //  none left
               continue;
            }

            if (Nstack > 1) {
               ii = Nstack - 2;                                            //  get prior pixel in stack
               ppx = pixstack[ii].px;
               ppy = pixstack[ii].py;
            }
            else {
               ppx = px - 1;                                               //  if only one, assume prior = left
               ppy = py;
            }
            
            dx = px - ppx;                                                 //  vector from prior to this pixel
            dy = py - ppy;
            
            switch (direc) 
            {
               case 0:
                  npx = px + dx;
                  npy = py + dy;
                  pixstack[kk].direc = 1;
                  break;

               case 1:
                  npx = px + dy;
                  npy = py + dx;
                  pixstack[kk].direc = 3;
                  break;
            
               case 2:
                  npx = px - dx;                                           //  back to prior pixel
                  npy = py - dy;                                           //  (this path never taken)
                  zappcrash("stack search bug");
                  break;
            
               case 3:
                  npx = px - dy;
                  npy = py - dx;
                  pixstack[kk].direc = 4;
                  break;
            
               case 4:
                  npx = px - dx;
                  npy = py + dy;
                  pixstack[kk].direc = 5;
                  break;
            
               case 5:
                  npx = px - dy;
                  npy = py + dx;
                  pixstack[kk].direc = 6;
                  break;
            
               case 6:
                  npx = px + dx;
                  npy = py - dy;
                  pixstack[kk].direc = 7;
                  break;
            
               case 7:
                  npx = px + dy;
                  npy = py - dx;
                  pixstack[kk].direc = 'x';
                  break;
               
               default:
                  npx = npy = 0;
                  zappcrash("stack search bug");
            }

            if (npx < 0 || npx > Fww-1) continue;                          //  pixel off the edge
            if (npy < 0 || npy > Fhh-1) continue;
            
            ii = npy * Fww + npx;
            if (pixgroup[ii]) continue;                                    //  pixel already assigned
            if (Factivearea && ! sa_pixmap[ii]) continue;                  //  pixel outside area

            pix3 = PXMpix(E3pxm16,npx,npy);                                //  pixel brightness
            pbright = ff * pixbright(pix3);
            if (pbright > brightness) continue;                            //  brighter than limit

            pixgroup[ii] = group;                                          //  assign pixel to group
            ++groupcount[group];                                           //  count pixels in group
            groupbright[group] += pbright;                                 //  sum brightness for group

            kk = Nstack++;                                                 //  put pixel into stack
            pixstack[kk].px = npx;
            pixstack[kk].py = npy;
            pixstack[kk].direc = 0;                                        //  search direction
         }
      }
      
      Ngroups = group;                                                     //  group numbers are 1-Ngroups
      Nremoved = 0;
      
      for (py = 0; py < Fhh; py++)                                         //  loop all pixels
      for (px = 0; px < Fww; px++)
      {
         ii = py * Fww + px;
         group = pixgroup[ii];
         if (! group) continue;
         if (groupspann[group].px1 == 0) {                                 //  first pixel found in this group
            groupspann[group].px1 = px;                                    //  group px1/py1 = this pixel
            groupspann[group].py1 = py;
            continue;
         }
         xspann = groupspann[group].px1 - px;                              //  spann from group px1/py1 to this pixel
         yspann = groupspann[group].py1 - py;
         spann2 = xspann * xspann + yspann * yspann;
         if (spann2 > groupspann[group].spann2) {   
            groupspann[group].spann2 = spann2;                             //  if greater, group px2/py2 = this pixel
            groupspann[group].px2 = px;
            groupspann[group].py2 = py;
         }
      }

      for (py = 0; py < Fhh; py++)                                         //  loop all pixels
      for (px = 0; px < Fww; px++)
      {
         ii = py * Fww + px;
         group = pixgroup[ii];
         if (! group) continue;
         if (groupspann[group].spann2 > spotspann2) continue;
         xspann = groupspann[group].px2 - px;                              //  spann from this pixel to group px2/py2
         yspann = groupspann[group].py2 - py;
         spann2 = xspann * xspann + yspann * yspann;
         if (spann2 > groupspann[group].spann2) {   
            groupspann[group].spann2 = spann2;                             //  if greater, group px1/py1 = this pixel
            groupspann[group].px1 = px;
            groupspann[group].py1 = py;
         }
      }

      for (py = 0; py < Fhh; py++)                                         //  loop all pixels
      for (px = 0; px < Fww; px++)
      {
         ii = py * Fww + px;                                               //  eliminate group if spann > limit
         group = pixgroup[ii];
         if (! group) continue;
         if (! groupcount[group]) pixgroup[ii] = 0;
         else if (groupspann[group].spann2 > spotspann2) {
            pixgroup[ii] = 0;
            groupcount[group] = 0;
            Nremoved++;
         }
      }
      
      for (py = 1; py < Fhh-1; py++)                                       //  loop all pixels except image edges
      for (px = 1; px < Fww-1; px++)
      {
         ii = py * Fww + px;
         group = pixgroup[ii];
         if (group) continue;                                              //  find pixels bordering group pixels
         pix3 = PXMpix(E3pxm16,px,py);
         pbright = ff * pixbright(pix3);
         
         group = pixgroup[ii-Fww-1];
         if (group) {
            ++edgecount[group];                                            //  accumulate pixel count and
            edgebright[group] += pbright;                                  //      bordering the groups
         }

         group = pixgroup[ii-Fww];
         if (group) {
            ++edgecount[group];
            edgebright[group] += pbright;
         }

         group = pixgroup[ii-Fww+1];
         if (group) {
            ++edgecount[group];
            edgebright[group] += pbright;
         }

         group = pixgroup[ii-1];
         if (group) {
            ++edgecount[group];
            edgebright[group] += pbright;
         }

         group = pixgroup[ii+1];
         if (group) {
            ++edgecount[group];
            edgebright[group] += pbright;
         }

         group = pixgroup[ii+Fww-1];
         if (group) {
            ++edgecount[group];
            edgebright[group] += pbright;
         }

         group = pixgroup[ii+Fww];
         if (group) {
            ++edgecount[group];
            edgebright[group] += pbright;
         }

         group = pixgroup[ii+Fww+1];
         if (group) {
            ++edgecount[group];
            edgebright[group] += pbright;
         }
      }

      for (group = 1; group <= Ngroups; group++)                           //  compute group pixel and edge pixel
      {                                                                    //    mean brightness
         if (groupcount[group] && edgecount[group]) {
            edgebright[group] = edgebright[group] / edgecount[group];
            groupbright[group] = groupbright[group] / groupcount[group];
            pcontrast = edgebright[group] - groupbright[group];            //  edge - group contrast
            if (pcontrast < contrast) {
               groupcount[group] = 0;
               Nremoved++;
            }
         }
      }

      for (py = 0; py < Fhh; py++)                                         //  loop all pixels
      for (px = 0; px < Fww; px++)
      {
         ii = py * Fww + px;                                               //  eliminate group if low contrast
         group = pixgroup[ii];
         if (! group) continue;
         if (! groupcount[group]) pixgroup[ii] = 0;
      }

      for (py = 0; py < Fhh; py++)                                         //  loop all pixels
      for (px = 0; px < Fww; px++)
      {
         ii = py * Fww + px;
         if (! pixgroup[ii]) continue;                                     //  not a dust pixel
         pix3 = PXMpix(E3pxm16,px,py);                                     //  paint it red
         pix3[0] = 65535;
         pix3[1] = pix3[2] = 0;
      }

      Fred = 1;
      mwpaint2();                                                          //  update window
   }

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


//  erase the selected dust areas

void dust_erase()
{
   using namespace dust_names;
   
   int         cc, ii, px, py, inc;
   int         qx, qy, npx, npy;
   int         sx, sy, tx, ty;
   int         rad, dist, dist2, mindist2;
   double      slope, f1, f2;
   uint16      *pix1, *pix3;
   char        *pmap;

   Ffuncbusy++;

   mutex_lock(&Fpixmap_lock);                                              //  image 3 = copy of image 9
   PXM_free(E3pxm16);
   E3pxm16 = PXM_copy(E9pxm16);
   mutex_unlock(&Fpixmap_lock);

   cc = Fww * Fhh;                                                         //  allocate pixel done map
   pmap = (char *) zmalloc(cc,"erase_dust");
   memset(pmap,0,cc);

   for (py = 0; py < Fhh; py++)                                            //  loop all pixels
   for (px = 0; px < Fww; px++)
   {
      ii = py * Fww + px;
      if (! pixgroup[ii]) continue;                                        //  not a dust pixel
      if (pmap[ii]) continue;                                              //  skip pixels already done
      
      mindist2 = 999999;
      npx = npy = 0;

      for (rad = 1; rad < 10; rad++)                                       //  find nearest edge (10 pixel limit)
      {
         for (qx = px-rad; qx <= px+rad; qx++)                             //  search within rad
         for (qy = py-rad; qy <= py+rad; qy++)
         {
            if (qx < 0 || qx >= Fww) continue;                             //  off image edge      modified  v.11.09
            if (qy < 0 || qy >= Fhh) continue;
            ii = qy * Fww + qx;
            if (pixgroup[ii]) continue;                                    //  within dust area

            dist2 = (px-qx) * (px-qx) + (py-qy) * (py-qy);                 //  distance**2 to edge pixel
            if (dist2 < mindist2) {
               mindist2 = dist2;
               npx = qx;                                                   //  save nearest pixel found
               npy = qy;
            }
         }

         if (rad * rad >= mindist2) break;                                 //  can quit now
      }

      if (! npx && ! npy) continue;                                        //  should not happen

      qx = npx;                                                            //  nearest edge pixel
      qy = npy;

      if (abs(qy - py) > abs(qx - px)) {                                   //  qx/qy = near edge from px/py
         slope = 1.0 * (qx - px) / (qy - py);
         if (qy > py) inc = 1;
         else inc = -1;
         for (sy = py; sy != qy+inc; sy += inc)                            //  line from px/py to qx/qy
         {
            sx = px + slope * (sy - py);
            ii = sy * Fww + sx;
            if (pmap[ii]) continue;                                        //  v.11.06
            pmap[ii] = 1;
            tx = qx + (qx - sx);                                           //  tx/ty = parallel line from qx/qy 
            ty = qy + (qy - sy);                                           //  modified  v.11.09
            if (tx < 0) tx = 0;
            if (tx > Fww-1) tx = Fww-1;
            if (ty < 0) ty = 0;
            if (ty > Fhh-1) ty = Fhh-1;
            pix1 = PXMpix(E3pxm16,tx,ty);                                  //  copy pixel from tx/ty to sx/sy
            pix3 = PXMpix(E3pxm16,sx,sy);
            pix3[0] = pix1[0];
            pix3[1] = pix1[1];
            pix3[2] = pix1[2];
         }
      }

      else {
         slope = 1.0 * (qy - py) / (qx - px);
         if (qx > px) inc = 1;
         else inc = -1;
         for (sx = px; sx != qx+inc; sx += inc)
         {
            sy = py + slope * (sx - px);
            ii = sy * Fww + sx;
            if (pmap[ii]) continue;
            pmap[ii] = 1;
            tx = qx + (qx - sx);
            ty = qy + (qy - sy);
            if (tx < 0) tx = 0;
            if (tx > Fww-1) tx = Fww-1;
            if (ty < 0) ty = 0;
            if (ty > Fhh-1) ty = Fhh-1;
            pix1 = PXMpix(E3pxm16,tx,ty);
            pix3 = PXMpix(E3pxm16,sx,sy);
            pix3[0] = pix1[0];
            pix3[1] = pix1[1];
            pix3[2] = pix1[2];
         }
      }
   }

   zfree(pmap);

   if (Factivearea)                                                        //  area edge blending
   {
      for (ii = 0; ii < Fww * Fhh; ii++)                                   //  find pixels in select area
      {
         dist = sa_pixmap[ii];
         if (! dist || dist >= sa_blend) continue;

         py = ii / Fww;
         px = ii - py * Fww;
         pix1 = PXMpix(E1pxm16,px,py);                                     //  input pixel, unchanged image
         pix3 = PXMpix(E3pxm16,px,py);                                     //  output pixel, changed image

         f2 = 1.0 * dist / sa_blend;                                       //  changes over distance sa_blend
         f1 = 1.0 - f2;

         pix3[0] = f1 * pix1[0] + f2 * pix3[0];                            //  blend the pixels
         pix3[1] = f1 * pix1[1] + f2 * pix3[1];
         pix3[2] = f1 * pix1[2] + f2 * pix3[2];
      }
   }

   Fred = 0;
   Ffuncbusy--;
   mwpaint2();                                                             //  update window
   return;
}


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

//  Find and fix stuck pixels (always bright or dark) from camera sensor defects.

namespace stuckpix 
{
   editfunc    EFstuckpix;
   int         stuckpix_1x1, stuckpix_2x2, stuckpix_3x3;                   //  pixel blocks to search
   int         stuckpix_color;                                             //  1/2/3 = white/black/red circles
   cchar       *stuckpix_mode;                                             //  find or fix
   double      stuckpix_threshcon;                                         //  min. contrast threshold
   char        *stuckpix_file = 0;                                         //  file for saved bad pixels

   struct badpix_t {                                                       //  memory for bad pixels
      int      px, py;                                                     //  location (NW corner of block)
      int      size;                                                       //  size: 1/2/3 = 1x1/2x2/3x3 block
      double   pcon;                                                       //  contrast with surrounding pixels
      int      rgb[3];                                                     //  surrounding pixel mean RGB
   };
   badpix_t    badpix[50];
   int         maxbad = 50, Nbad;
}


void m_stuckpix(GtkWidget *, cchar *menu)                                  //  new v.11.12
{
   using namespace stuckpix;

   int    stuckpix_dialog_event(zdialog *zd, cchar *event);
   void * stuckpix_thread(void *);

   zdialog     *zd;

   zfuncs::F1_help_topic = "stuck_pixels";

   EFstuckpix.funcname = "stuckpix";                                       //  function name
   EFstuckpix.threadfunc = stuckpix_thread;                                //  thread function

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

/***

   pixel group   [x] 1x1  [x] 2x2  [x] 3x3
   circle color  (o) white  (o) black  (o) red
   contrast      =========[]====================

                  [open] [save] [apply] [cancel]
***/

   zd = zdialog_new(ZTX("Fix Stuck Pixels"),mWin,Bopen,Bsave,Bapply,Bcancel,null);
   EFstuckpix.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=5|homog");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"space=5|expand|homog");
   zdialog_add_widget(zd,"label","labgroup","vb1",ZTX("pixel group"));
   zdialog_add_widget(zd,"label","labcolor","vb1",ZTX("circle color"));
   zdialog_add_widget(zd,"label","labcont","vb1",ZTX("contrast"));
   zdialog_add_widget(zd,"hbox","hbgroup","vb2");
   zdialog_add_widget(zd,"check","1x1","hbgroup","1x1","space=3");
   zdialog_add_widget(zd,"check","2x2","hbgroup","2x2","space=3");
   zdialog_add_widget(zd,"check","3x3","hbgroup","3x3","space=3");
   zdialog_add_widget(zd,"hbox","hbcolor","vb2");
   zdialog_add_widget(zd,"radio","white","hbcolor",Bwhite,"space=5");
   zdialog_add_widget(zd,"radio","black","hbcolor",Bblack,"space=5");
   zdialog_add_widget(zd,"radio","red","hbcolor",Bred,"space=5");
   zdialog_add_widget(zd,"hscale","contrast","vb2","0.1|1.0|0.001|0.5","expand");
   
   zdialog_stuff(zd,"1x1",1);                                              //  initz. dialog controls
   zdialog_stuff(zd,"2x2",1);
   zdialog_stuff(zd,"3x3",1);
   zdialog_stuff(zd,"white",0);
   zdialog_stuff(zd,"black",0);
   zdialog_stuff(zd,"red",1);
   
   stuckpix_1x1 = stuckpix_2x2 = stuckpix_3x3 = 1;                         //  corresp. data values
   stuckpix_color = 3;
   stuckpix_threshcon = 0.5;
   
   if (! stuckpix_file) {                                                  //  default file
      stuckpix_file = zmalloc(200);                                        //  /home/<user>/.fotoxx/stuck-pixels
      snprintf(stuckpix_file,200,"%s/stuck-pixels",get_zuserdir());
   }

   zdialog_help(zd,"stuck_pixels");
   zdialog_run(zd,stuckpix_dialog_event,"save");                           //  run dialog

   stuckpix_mode = "find";
   signal_thread();                                                        //  find and show the bad pixels
   wait_thread_idle();

   return;
}


//  dialog event function

int stuckpix_dialog_event(zdialog *zd, cchar *event)
{
   using namespace stuckpix;
   
   void  stuckpix_open();
   void  stuckpix_save();

   int      color;
   
   if (zd->zstat)                                                          //  completion button
   {
      if (zd->zstat == 1) {                                                //  open file
         zd->zstat = 0;                                                    //  keep dialog active
         stuckpix_open();
      }

      else if (zd->zstat == 2) {                                           //  save file
         zd->zstat = 0;                                                    //  keep dialog active
         stuckpix_save();
      }

      else if (zd->zstat == 3) {                                           //  apply
         stuckpix_mode = "apply";
         signal_thread();                                                  //  fix the bad pixels
         wait_thread_idle();
         edit_done(EFstuckpix);
         erase_topcircles();
      }

      else {                                                               //  cancel
         edit_cancel(EFstuckpix);
         erase_topcircles();
      }

      mwpaint2();                                                          //  update main window
      return 0;
   }

   zdialog_fetch(zd,"1x1",stuckpix_1x1);                                   //  get dialog inputs
   zdialog_fetch(zd,"2x2",stuckpix_2x2);
   zdialog_fetch(zd,"3x3",stuckpix_3x3);
   
   zdialog_fetch(zd,"white",color);
   if (color) stuckpix_color = 1;
   zdialog_fetch(zd,"black",color);
   if (color) stuckpix_color = 2;
   zdialog_fetch(zd,"red",color);
   if (color) stuckpix_color = 3;

   zdialog_fetch(zd,"contrast",stuckpix_threshcon);
   
   stuckpix_mode = "find";
   signal_thread();                                                        //  find and show bad pixels
   wait_thread_idle();

   return 0;
}


//  load bad pixel list from a previously saved file

void stuckpix_open()
{
   using namespace stuckpix;

   int  stuckpix_open_dialog_event(zdialog *zd, cchar *event);

   zdialog     *zd;
   
   zd = zdialog_new(ZTX("Load Stuck Pixels"),mWin,Bopen,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",ZTX("File:"),"space=3");
   zdialog_add_widget(zd,"entry","file","hb1",0,"expand|scc=40");
   zdialog_add_widget(zd,"button","browse","hb1",Bbrowse,"space=5");
   
   zdialog_stuff(zd,"file",stuckpix_file);
   
   zdialog_run(zd,stuckpix_open_dialog_event);
   zdialog_wait(zd);

   return;
}

   
int stuckpix_open_dialog_event(zdialog *zd, cchar *event)
{
   using namespace stuckpix;

   char     *pp, file[200];
   FILE     *fid = 0;
   int      zstat, nn, ii, px, py, size;
   
   if (strEqu(event,"browse")) {
      pp = zgetfile1(ZTX("Stuck Pixels file"),"open",stuckpix_file);
      if (! pp) return 0;
      zdialog_stuff(zd,"file",pp);
      zfree(pp);
   }
   
   zstat = zd->zstat;                                                      //  completion button
   
   if (zstat)
   {
      if (zstat == 1)                                                      //  open
      {
         zdialog_fetch(zd,"file",file,200);                                //  get file from dialog
         if (stuckpix_file) zfree(stuckpix_file);
         stuckpix_file = strdupz(file);

         fid = fopen(file,"r");                                            //  open file
         if (! fid) {
            zmessageACK(mWin,ZTX("file not found"));
            return 1;
         }
         
         nn = fscanf(fid,"stuck pixels px py size");                       //  read headers
         
         for (ii = 0; ii < maxbad; ii++)
         {
            nn = fscanf(fid," %5d %5d %5d ",&px,&py,&size);                //  read bad pixels data
            if (nn == EOF) break;
            if (nn != 3) break;
            badpix[ii].px = px;
            badpix[ii].py = py;
            badpix[ii].size = size;
            badpix[ii].pcon = 0;
         }
         
         Nbad = ii;
         fclose(fid);
         zdialog_free(zd);

         if (! Nbad || nn != EOF) 
            zmessageACK(mWin,ZTX("file format error"));

         stuckpix_mode = "file";                                           //  process pixels
         signal_thread();
         wait_thread_idle();
      }
      
      else {                                                               //  cancel or [x]
         zdialog_free(zd);
         return 1;
      }
   }

   return 1;
}


//  save bad pixel list to a file or add them to a previous file

void stuckpix_save()
{
   using namespace stuckpix;

   int  stuckpix_save_dialog_event(zdialog *zd, cchar *event);

   zdialog     *zd;
   
   if (Nbad == 0) {
      zmessageACK(mWin,ZTX("there are zero stuck pixels"));
      return;
   }
   
   zd = zdialog_new(ZTX("Save Stuck Pixels"),mWin,Bsave,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",ZTX("File:"),"space=3");
   zdialog_add_widget(zd,"entry","file","hb1",0,"expand|scc=40");
   zdialog_add_widget(zd,"button","browse","hb1",Bbrowse,"space=5");
   
   zdialog_stuff(zd,"file",stuckpix_file);
   
   zdialog_run(zd,stuckpix_save_dialog_event);
   zdialog_wait(zd);

   return;
}


int stuckpix_save_dialog_event(zdialog *zd, cchar *event)
{
   using namespace stuckpix;

   char     *pp, file[200];
   FILE     *fid = 0;
   int      zstat, ii, px, py, size;
   
   if (strEqu(event,"browse")) {
      pp = zgetfile1(ZTX("Stuck Pixels file"),"save",stuckpix_file);
      if (! pp) return 0;
      zdialog_stuff(zd,"file",pp);
      zfree(pp);
   }
   
   zstat = zd->zstat;                                                      //  completion button
   
   if (zstat)
   {
      if (zstat == 1)                                                      //  save
      {
         zdialog_fetch(zd,"file",file,200);                                //  get file from dialog
         if (stuckpix_file) zfree(stuckpix_file);
         stuckpix_file = strdupz(file);

         fid = fopen(file,"w");                                            //  open file
         if (! fid) {
            zmessageACK(mWin,strerror(errno));
            return 1;
         }
         
         fprintf(fid,"stuck pixels \n");                                   //  write headers
         fprintf(fid,"    px    py   size \n");
         
         for (ii = 0; ii < Nbad; ii++)                                     //  write bad pixel data
         {
            px = badpix[ii].px;
            py = badpix[ii].py;
            size = badpix[ii].size;
            fprintf(fid," %5d %5d %5d \n",px,py,size);
         }

         fclose(fid);
         zdialog_free(zd);
      }

      else {                                                               //  cancel or [x]
         zdialog_free(zd);
         return 1;
      }
   }

   return 1;
}


//  perform the fix function
//  find pixel groups with contrast exceeding the limit
//  replace these pixels with surrounding ones

void * stuckpix_thread(void *)
{
   using namespace stuckpix;

   void   stuckpix_pixelblock(int px, int py, int size, int rgb[3]);
   void   stuckpix_surroundings(int px, int py, int size, int rgb[3]);
   double stuckpix_getcon(int rgbA[3], int rgbB[3]);
   void   stuckpix_insert(int px, int py, int size, double pcon, int rgb[3]);
   void   stuckpix_show();

   int         px, py, qx, qy, ii, size;
   int         rgbA[3], rgbB[3];
   double      threshcon, pcon;
   uint16      *ppix;
   
   while (true)
   {
      thread_idle_loop();
   
      if (strEqu(stuckpix_mode,"find"))                                    //  find bad pixels in image
      {
         threshcon = stuckpix_threshcon;                                   //  threshold contrast, 0.1 to 1.0
         Nbad = 0;                                                         //  count bad pixels found

         if (stuckpix_3x3)                                                 //  find 3x3 pixel groups FIRST
         {
            for (py = 2; py < E3hh-5; py++)
            for (px = 2; px < E3ww-5; px++)
            {
               stuckpix_pixelblock(px,py,3,rgbA);                          //  get mean RGB for 3x3 pixel block
               stuckpix_surroundings(px,py,3,rgbB);                        //  get surrounding pixels mean RGB
               pcon = stuckpix_getcon(rgbA,rgbB);                          //  contrast with surrounding
               if (pcon > threshcon) 
                  stuckpix_insert(px,py,3,pcon,rgbB);                      //  if > threshold, add to table
               if (Nbad == maxbad) goto findquit;
            }
         }

         if (stuckpix_2x2)                                                 //  find 2x2 pixel groups
         {
            for (py = 2; py < E3hh-4; py++)
            for (px = 2; px < E3ww-4; px++)
            {
               stuckpix_pixelblock(px,py,2,rgbA);                          //  get mean RGB for 2x2 pixel block
               stuckpix_surroundings(px,py,2,rgbB);                        //  get surrounding pixels mean RGB
               pcon = stuckpix_getcon(rgbA,rgbB);                          //  contrast with surrounding
               if (pcon > threshcon) 
                  stuckpix_insert(px,py,2,pcon,rgbB);                      //  if > threshold, add to table
               if (Nbad == maxbad) goto findquit;
            }
         }

         if (stuckpix_1x1)                                                 //  find 1x1 pixel groups LAST
         {
            for (py = 2; py < E3hh-3; py++)
            for (px = 2; px < E3ww-3; px++)
            {
               stuckpix_pixelblock(px,py,1,rgbA);                          //  get mean RGB for 1x1 pixel block
               stuckpix_surroundings(px,py,1,rgbB);                        //  get surrounding pixels mean RGB
               pcon = stuckpix_getcon(rgbA,rgbB);                          //  contrast with surrounding
               if (pcon > threshcon) 
                  stuckpix_insert(px,py,1,pcon,rgbB);                      //  if > threshold, add to table
               if (Nbad == maxbad) goto findquit;
            }
         }

         findquit:
         stuckpix_show();                                                  //  show the bad pixels found
      }

      if (strEqu(stuckpix_mode,"file"))                                    //  process bad pixels read from a file
      {
         for (ii = 0; ii < Nbad; ii++)
         {
            px = badpix[ii].px;
            py = badpix[ii].py;
            size = badpix[ii].size;
            
            stuckpix_pixelblock(px,py,size,rgbA);
            stuckpix_surroundings(px,py,size,rgbB);
            pcon = stuckpix_getcon(rgbA,rgbB);
            
            badpix[ii].pcon = pcon;
            badpix[ii].rgb[0] = rgbB[0];
            badpix[ii].rgb[1] = rgbB[1];
            badpix[ii].rgb[2] = rgbB[2];
         }
      
         stuckpix_show();                                                  //  show the bad pixels read
      }

      if (strEqu(stuckpix_mode,"apply"))                                   //  replace the bad pixels 
      {
         for (ii = 0; ii < Nbad; ii++)                                     //  loop pixel groups found
         {
            px = badpix[ii].px;
            py = badpix[ii].py;
            size = badpix[ii].size;
            if (! size) continue;
            
            if (size == 1)                                                 //  1x1 pixel group
            {
               ppix = PXMpix(E3pxm16,px,py);                               //  replace pixel group
               ppix[0] = badpix[ii].rgb[0];
               ppix[1] = badpix[ii].rgb[1];
               ppix[2] = badpix[ii].rgb[2];
            }
            
            if (size == 2)                                                 //  2x2 pixel group
            {
               for (qy = py; qy < py+2; qy++)                              //  replace pixel group
               for (qx = px; qx < px+2; qx++)
               {
                  ppix = PXMpix(E3pxm16,qx,qy);
                  ppix[0] = badpix[ii].rgb[0];
                  ppix[1] = badpix[ii].rgb[1];
                  ppix[2] = badpix[ii].rgb[2];
               }
            }

            if (size == 3)                                                 //  3x3 pixel group
            {
               for (qy = py; qy < py+3; qy++)                              //  replace pixel group
               for (qx = px; qx < px+3; qx++)
               {
                  ppix = PXMpix(E3pxm16,qx,qy);
                  ppix[0] = badpix[ii].rgb[0];
                  ppix[1] = badpix[ii].rgb[1];
                  ppix[2] = badpix[ii].rgb[2];
               }
            }
         }
         
         EFstuckpix.Fmod++;                                                //  image modified
         mwpaint2();                                                       //  update window
      }
   }

   return 0;
}


//  get the mean RGB values for a block of (defective) pixels

void stuckpix_pixelblock(int px, int py, int size, int rgb[3])
{
   uint16   *ppix;
   
   ppix = PXMpix(E3pxm16,px,py);                                           //  block of pixels

   if (size == 1)                                                          //  1x1 block, 1 pixel
   {
      rgb[0] = ppix[0];
      rgb[1] = ppix[1];
      rgb[2] = ppix[2];
   }

   if (size == 2)                                                          //  2x2 block, 4 pixels
   {
      rgb[0] = (ppix[0] + ppix[3] + ppix[E3ww*3]   + ppix[E3ww*3+3]) / 4;
      rgb[1] = (ppix[1] + ppix[4] + ppix[E3ww*3+1] + ppix[E3ww*3+4]) / 4;
      rgb[2] = (ppix[2] + ppix[5] + ppix[E3ww*3+2] + ppix[E3ww*3+5]) / 4;
   }

   if (size == 3)                                                          //  3x3 block, 9 pixels
   {
      rgb[0] = (ppix[0] + ppix[3] + ppix[6]
              +  ppix[E3ww*3] + ppix[E3ww*3+3] + ppix[E3ww*3+6]
              +  ppix[E3ww*6] + ppix[E3ww*6+3] + ppix[E3ww*6+6]) / 9;

      rgb[1] = (ppix[1] + ppix[4] + ppix[7]
              +  ppix[E3ww*3+1] + ppix[E3ww*3+4] + ppix[E3ww*3+7]
              +  ppix[E3ww*6+1] + ppix[E3ww*6+4] + ppix[E3ww*6+7]) / 9;

      rgb[2] = (ppix[2] + ppix[5] + ppix[8]
              +  ppix[E3ww*3+2] + ppix[E3ww*3+5] + ppix[E3ww*3+8]
              +  ppix[E3ww*6+2] + ppix[E3ww*6+5] + ppix[E3ww*6+8]) / 9;
   }

   return;
}


//  get the mean RGB values for pixels surrounding a given pixel block

void stuckpix_surroundings(int px, int py, int size, int rgb[3])
{
   int      qx, qy, ii;
   int      red, green, blue;
   uint16   *ppix;

   int   n8x[8] = { -1, 0, 1,-1, 1,-1, 0, 1 };                             //  8 neighbors of 1x1 group at [0,0]
   int   n8y[8] = { -1,-1,-1, 0, 0, 1, 1, 1 };

   int   n12x[12] = { -1, 0, 1, 2,-1, 2,-1, 2,-1, 0, 1, 2 };               //  12 neighbors of 2x2 group at [0,0]
   int   n12y[12] = { -1,-1,-1,-1, 0, 0, 1, 1, 2, 2, 2, 2 };

   int   n16x[16] = { -1, 0, 1, 2, 3,-1, 3,-1, 3,-1, 3,-1, 0, 1, 2, 3 };   //  16 neighbors of 3x3 group at [0,0]
   int   n16y[16] = { -1,-1,-1,-1,-1, 0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 3 };

   red = green = blue = 0;

   if (size == 1)
   {
      for (ii = 0; ii < 8; ii++)                                           //  surrounding 8 pixels
      {
         qx = px + n8x[ii];
         qy = py + n8y[ii];
         ppix = PXMpix(E3pxm16,qx,qy);
         red += ppix[0];
         green += ppix[1];
         blue += ppix[2];
      }
      
      red = red / 8;                                                       //  average surrounding pixels                  
      green = green / 8;
      blue = blue / 8;
   }

   if (size == 2)
   {
      for (ii = 0; ii < 12; ii++)                                          //  surrounding 12 pixels
      {
         qx = px + n12x[ii];
         qy = py + n12y[ii];
         ppix = PXMpix(E3pxm16,qx,qy);
         red += ppix[0];
         green += ppix[1];
         blue += ppix[2];
      }
      
      red = red / 12;                                                      //  average surrounding pixels                  
      green = green / 12;
      blue = blue / 12;
   }

   if (size == 3)
   {
      for (ii = 0; ii < 16; ii++)                                          //  surrounding 16 pixels
      {
         qx = px + n16x[ii];
         qy = py + n16y[ii];
         ppix = PXMpix(E3pxm16,qx,qy);
         red += ppix[0];
         green += ppix[1];
         blue += ppix[2];
      }
      
      red = red / 16;                                                      //  average surrounding pixels                  
      green = green / 16;
      blue = blue / 16;
   }
   
   rgb[0] = red;
   rgb[1] = green;
   rgb[2] = blue;
   
   return;
}


//  draw circles around the bad pixels

void stuckpix_show()
{
   using namespace stuckpix;

   int      ii, px, py, size, rad;

   erase_topcircles();                                                     //  erase prior circles

   for (ii = 0; ii < Nbad; ii++)                                           //  write circles around bad pixels
   {
      px = badpix[ii].px;
      py = badpix[ii].py;
      size = badpix[ii].size;
      if (! size) continue;
      rad = 8 + 2 * size;
      px += size / 2;
      py += size / 2;
      add_topcircle(px,py,rad,stuckpix_color);
   }
   
   mwpaint2();                                                             //  update window
   return;
}


//  compare two pixels and return contrast
//  0 = no contrast, 1 = max. contrast (black:white)

double stuckpix_getcon(int rgbA[3], int rgbB[3])
{
   double      ff = 1.0 / 65536.0;
   double      Rdiff, Gdiff, Bdiff, match;

   Rdiff = ff * abs(rgbA[0] - rgbB[0]);                                    //  0 = perfect match
   Gdiff = ff * abs(rgbA[1] - rgbB[1]);                                    //  1 = total mismatch
   Bdiff = ff * abs(rgbA[2] - rgbB[2]);
   
   match = (1.0 - Rdiff) * (1.0 - Gdiff) * (1.0 - Bdiff);                  //  1 = perfect match
   return (1.0 - match);
}


//  Insert a new bad pixel block into the bad pixel table.
//  If the new entry touches on a previous entry,
//  then remove the entry with lesser contrast.

void stuckpix_insert(int px, int py, int size, double pcon, int rgb[3])
{
   using namespace stuckpix;
   
   int      ii, px1, py1, px2, py2, px3, py3, px4, py4;
   
   px1 = px - 1;                                                           //  "touch" periphery
   py1 = py - 1;
   px2 = px + size + 1;
   py2 = py + size + 1;
   
   for (ii = 0; ii < Nbad; ii++)                                           //  loop badpix table
   {
      if (badpix[ii].size == 0) continue;                                  //  skip deleted entry

      px3 = badpix[ii].px;
      py3 = badpix[ii].py;
      px4 = px3 + badpix[ii].size - 1;
      py4 = py3 + badpix[ii].size - 1;

      if ((px1 >= px3 && px1 <= px4 && py1 >= py3 && py1 <= py4) ||        //  test if old entry touches new
          (px2 >= px3 && px2 <= px4 && py2 >= py3 && py2 <= py4) ||
          (px1 >= px3 && px1 <= px4 && py2 >= py3 && py2 <= py4) ||
          (px2 >= px3 && px2 <= px4 && py1 >= py3 && py1 <= py4) ||
          (px3 >= px1 && px3 <= px2 && py3 >= py1 && py3 <= py2) ||
          (px4 >= px1 && px4 <= px2 && py4 >= py1 && py4 <= py2) ||
          (px3 >= px1 && px3 <= px2 && py4 >= py1 && py4 <= py2) ||
          (px4 >= px1 && px4 <= px2 && py3 >= py1 && py3 <= py2))
      {
         if (size < badpix[ii].size   &&                                   //  if new touches a larger block,
             pcon < 2.0 * badpix[ii].pcon) return;                         //    keep only if contrast much greater

         if (pcon < 1.0 * badpix[ii].pcon) return;                         //  keep only if contrast is greater

         badpix[ii].size = 0;                                              //  delete old entry
      }
   }

   for (ii = 0; ii < Nbad; ii++)                                           //  loop badpix table
      if (badpix[ii].size == 0) break;                                     //  find first empty slot or last + 1
   if (ii == maxbad) return;                                               //  table full
   
   badpix[ii].px = px;                                                     //  replace overlapping entry
   badpix[ii].py = py;                                                     //    or add to end of table
   badpix[ii].size = size;
   badpix[ii].pcon = pcon;
   badpix[ii].rgb[0] = rgb[0];
   badpix[ii].rgb[1] = rgb[1];
   badpix[ii].rgb[2] = rgb[2];

   if (ii == Nbad) Nbad++;                                                 //  incr. count if added to end
   if (Nbad == maxbad) printf("exceed %d bad pixels \n",maxbad);           //  report one time only

   return;
}


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

//  pixel edit function - edit individual pixels

#define pixed_undomaxmem (1000 * mega)                                     //  pixel edit max. memory          v.11.04
#define pixed_undomaxpix (mega)                                            //  pixel edit max. pixel blocks

void  pixed_mousefunc();
void  pixed_dopixels(int px, int py);
void  pixed_undo1();
void  pixed_freeundo();

int      pixed_RGB[3];
int      pixed_mode;
int      pixed_radius;
double   pixed_kernel[200][200];                                           //  radius <= 99

int      pixed_undototpix = 0;                                             //  total undo pixel blocks
int      pixed_undototmem = 0;                                             //  total undo memory allocated
int      pixed_undoseq = 0;                                                //  undo sequence no.
char     pixed_undomemmessage[100];                                        //  translated undo memory message

typedef struct {                                                           //  pixel block before edit
   int         seq;                                                        //  undo sequence no.
   uint16      npix;                                                       //  no. pixels in this block
   uint16      px, py;                                                     //  center pixel (radius org.)
   uint16      radius;                                                     //  radius of pixel block
   uint16      pixel[][3];                                                 //  array of pixel[npix][3] 
}  pixed_savepix;

pixed_savepix   **pixed_undopixmem = 0;                                    //  array of *pixed_savepix

editfunc    EFpixed;


void m_pixedit(GtkWidget *, cchar *)
{
   int   pixed_dialog_event(zdialog* zd, cchar *event);

   char        undomemmessage[100];

   zfuncs::F1_help_topic = "edit_pixels";                                  //  v.10.8

   EFpixed.funcname = "pixel-edit";
   EFpixed.Farea = 1;                                                      //  select area ignored
   EFpixed.Fpara = 1;                                                      //  parallel edit OK
   EFpixed.mousefunc = pixed_mousefunc;                                    //  mouse function
   if (! edit_setup(EFpixed)) return;                                      //  setup edit

   strncpy0(pixed_undomemmessage,ZTX("Undo Memory %d%c"),99);              //  translate undo memory message

   zdialog *zd = zdialog_new(ZTX("Edit Pixels"),mWin,Bdone,Bcancel,null);
   EFpixed.zd = zd;

   zdialog_add_widget(zd,"hbox","hbc","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labc","hbc",ZTX("color"),"space=8");
   zdialog_add_widget(zd,"colorbutt","color","hbc","100|100|100");
   zdialog_add_widget(zd,"label","space","hbc",0,"space=10");
   zdialog_add_widget(zd,"radio","radio1","hbc",ZTX("pick"),"space=3");
   zdialog_add_widget(zd,"radio","radio2","hbc",ZTX("paint"),"space=3");
   zdialog_add_widget(zd,"radio","radio3","hbc",ZTX("erase"),"space=3");
   zdialog_add_widget(zd,"hbox","hbbri","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbbr1","hbbri",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbbr2","hbbri",0,"homog|space=5");
   zdialog_add_widget(zd,"label","space","hbbri",0,"space=10");
   zdialog_add_widget(zd,"vbox","vbbr3","hbbri",0,"homog|space=3");
   zdialog_add_widget(zd,"hbox","hbrad","vbbr1",0,"space=3");
   zdialog_add_widget(zd,"label","space","hbrad",0,"expand");
   zdialog_add_widget(zd,"label","labbr","hbrad",ZTX("paintbrush radius"));
   zdialog_add_widget(zd,"label","labtc","vbbr1",ZTX("transparency center"));
   zdialog_add_widget(zd,"label","labte","vbbr1",ZTX("transparency edge"));
   zdialog_add_widget(zd,"spin","radius","vbbr2","1|99|1|5");
   zdialog_add_widget(zd,"spin","trcent","vbbr2","0|100|0.1|95");          //  smaller steps       v.11.10
   zdialog_add_widget(zd,"spin","tredge","vbbr2","0|100|0.1|100");
   zdialog_add_widget(zd,"button","undlast","vbbr3",Bundolast);
   zdialog_add_widget(zd,"button","undall","vbbr3",Bundoall);
   zdialog_add_widget(zd,"hbox","hb4","dialog");   
   zdialog_add_widget(zd,"label","labmem","hb4");

   zdialog_help(zd,"edit_pixels");                                         //  zdialog help topic              v.11.08
   zdialog_run(zd,pixed_dialog_event,"save");                              //  run dialog, parallel            v.11.07

   zdialog_send_event(zd,"radius");                                        //  get kernel initialized

   snprintf(undomemmessage,99,pixed_undomemmessage,0,'%');                 //  stuff undo memory status
   zdialog_stuff(zd,"labmem",undomemmessage);
   
   pixed_RGB[0] = pixed_RGB[1] = pixed_RGB[2] = 100;                       //  initialize color
   
   pixed_mode = 1;                                                         //  mode = pick color

   pixed_undopixmem = 0;                                                   //  no undo data
   pixed_undototpix = 0;
   pixed_undototmem = 0;
   pixed_undoseq = 0;

   takeMouse(zd,pixed_mousefunc,drawcursor);                               //  connect mouse function          v.11.03
   return;
}


//  dialog event and completion callback function

int pixed_dialog_event(zdialog *zd, cchar *event)                          //  pixedit dialog event function
{
   char        color[20];
   cchar       *pp;
   int         radius, dx, dy, brad;
   double      rad, kern, trcent, tredge;
   
   if (zd->zstat) 
   {
      if (zd->zstat == 1) edit_done(EFpixed);                              //  done
      else edit_cancel(EFpixed);                                           //  cancel or destroy
      pixed_freeundo();                                                    //  free undo memory
      return 0;
   }
   
   edit_takeover(EFpixed);                                                 //  set my edit function

   paint_toparc(2);                                                        //  v.11.04

   if (strEqu(event,"focus"))                                              //  toggle mouse capture         v.12.01
      takeMouse(zd,pixed_mousefunc,drawcursor);

   zdialog_fetch(zd,"radio1",brad);                                        //  pick
   if (brad) pixed_mode = 1;
   zdialog_fetch(zd,"radio2",brad);                                        //  paint
   if (brad) pixed_mode = 2;
   zdialog_fetch(zd,"radio3",brad);                                        //  erase
   if (brad) pixed_mode = 3;
   
   if (strEqu(event,"color")) 
   {
      zdialog_fetch(zd,"color",color,19);                                  //  get color from color wheel
      pp = strField(color,"|",1);
      if (pp) pixed_RGB[0] = atoi(pp);
      pp = strField(color,"|",2);
      if (pp) pixed_RGB[1] = atoi(pp);
      pp = strField(color,"|",3);
      if (pp) pixed_RGB[2] = atoi(pp);
   }
   
   if (strstr("radius trcent tredge",event))                               //  get new brush attributes
   {
      zdialog_fetch(zd,"radius",radius);                                   //  radius
      zdialog_fetch(zd,"trcent",trcent);                                   //  center transparency
      zdialog_fetch(zd,"tredge",tredge);                                   //  edge transparency

      pixed_radius = radius;
      trcent = 0.01 * trcent;                                              //  scale 0 ... 1
      tredge = 0.01 * tredge;
      tredge = (1 - trcent) * (1 - tredge);
      tredge = 1 - tredge;
      trcent = sqrt(trcent);                                               //  speed up the curve
      tredge = sqrt(tredge);

      for (dy = -radius; dy <= radius; dy++)                               //  build kernel
      for (dx = -radius; dx <= radius; dx++)
      {
         rad = sqrt(dx*dx + dy*dy);
         kern = (radius - rad) / radius;                                   //  1 ... 0 
         kern = kern * (trcent - tredge) + tredge;                         //  trcent ... tredge
         if (rad > radius) kern = 1;
         if (kern < 0) kern = 0;
         if (kern > 1) kern = 1;
         pixed_kernel[dx+radius][dy+radius] = kern;
      }
   }
   
   if (strEqu(event,"undlast"))                                            //  undo last edit (click or drag)
      pixed_undo1();

   if (strEqu(event,"undall")) {                                           //  undo all edits
      edit_reset();                                                        //  v.10.3
      pixed_freeundo();
   }

   return 1;
}


//  pixel edit mouse function

void pixed_mousefunc()
{
   static int  pmxdown = 0, pmydown = 0;
   int         px, py;
   char        color[20];
   uint16      *ppix3;
   zdialog     *zd = EFpixed.zd;

   edit_takeover(EFpixed);                                                 //  set my edit function

   if (LMclick)                                                            //  left mouse click
   {
      LMclick = 0;
      px = Mxclick;
      py = Myclick;

      if (pixed_mode == 1)                                                 //  pick new color from image
      {
         ppix3 = PXMpix(E3pxm16,px,py);
         pixed_RGB[0] = ppix3[0] / 256;
         pixed_RGB[1] = ppix3[1] / 256;
         pixed_RGB[2] = ppix3[2] / 256;
         snprintf(color,19,"%d|%d|%d",pixed_RGB[0],pixed_RGB[1],pixed_RGB[2]);
         if (zd) zdialog_stuff(zd,"color",color);
      }
      else {                                                               //  paint or erase
         pixed_undoseq++;                                                  //  new undo seq. no.
         paint_toparc(2);
         pixed_dopixels(px,py);                                            //  do 1 block of pixels
      }
   }
   
   if (RMclick) 
   {
      RMclick = 0;
      paint_toparc(2);
      pixed_undo1();                                                       //  undo last paint      v.10.11
   }
   
   if (Mxdrag || Mydrag)                                                   //  drag in progress
   {
      px = Mxdrag;
      py = Mydrag;
      Mxdrag = Mydrag = 0;

      if (Mxdown != pmxdown || Mydown != pmydown) {                        //  new drag
         pixed_undoseq++;                                                  //  new undo seq. no.
         pmxdown = Mxdown;
         pmydown = Mydown;
      }
      paint_toparc(2);
      pixed_dopixels(px,py);                                               //  do 1 block of pixels
   }
   
   toparcx = Mxposn - pixed_radius;                                        //  define brush outline circle
   toparcy = Myposn - pixed_radius;
   toparcw = toparch = 2 * pixed_radius;

   if (pixed_mode == 1) Ftoparc = 0;
   else Ftoparc = 1;
   if (Ftoparc) paint_toparc(3);
   
   return;
}


//  paint or erase 1 block of pixels within radius of px, py

void pixed_dopixels(int px, int py)
{
   void  pixed_saveundo(int px, int py);

   uint16      *ppix1, *ppix3;
   int         radius, dx, dy, qx, qy, ii, ww, dist;
   int         red, green, blue;
   double      kern;

   edit_zapredo();                                                         //  delete redo copy    v.10.3
   
   pixed_saveundo(px,py);                                                  //  save pixels for poss. undo   v.10.12.1

   red = 256 * pixed_RGB[0];
   green = 256 * pixed_RGB[1];
   blue = 256 * pixed_RGB[2];

   radius = pixed_radius;

   for (dy = -radius; dy <= radius; dy++)                                  //  loop surrounding block of pixels
   for (dx = -radius; dx <= radius; dx++)
   {
      qx = px + dx;
      qy = py + dy;
      
      if (qx < 0 || qx > E3ww-1) continue;
      if (qy < 0 || qy > E3hh-1) continue;
      
      if (Factivearea) {                                                   //  select area active     v.10.11
         ii = qy * E3ww + qx;
         dist = sa_pixmap[ii];
         if (! dist) continue;                                             //  pixel is outside area
      }
      
      kern = pixed_kernel[dx+radius][dy+radius];
      ppix1 = PXMpix(E1pxm16,qx,qy);                                       //  original image pixel
      ppix3 = PXMpix(E3pxm16,qx,qy);                                       //  edited image pixel

      if (pixed_mode == 2)                                                 //  color pixels transparently
      {
         ppix3[0] = (1.0 - kern) * red   + kern * ppix3[0];
         ppix3[1] = (1.0 - kern) * green + kern * ppix3[1];
         ppix3[2] = (1.0 - kern) * blue  + kern * ppix3[2];
         CEF->Fmod = 1;
      }

      if (pixed_mode == 3)                                                 //  restore org. pixels transparently
      {
         ppix3[0] = (1.0 - kern) * ppix1[0] + kern * ppix3[0];
         ppix3[1] = (1.0 - kern) * ppix1[1] + kern * ppix3[1];
         ppix3[2] = (1.0 - kern) * ppix1[2] + kern * ppix3[2];
      }
   }

   px = px - radius - 1;
   py = py - radius - 1;
   ww = 2 * radius + 3;
   mwpaint3(px,py,ww,ww);                                                  //  v.10.11
   return;
}


//  save 1 block of pixels for possible undo

void pixed_saveundo(int px, int py)
{
   int            npix, radius, dx, dy;
   uint16         *ppix3;
   pixed_savepix  *ppixsave1;
   char           undomemmessage[100];
   int            mempercent;
   static int     ppercent = 0;
   zdialog        *zd = EFpixed.zd;

   if (! pixed_undopixmem)                                                 //  first call
   {
      pixed_undopixmem = (pixed_savepix **) zmalloc(pixed_undomaxpix * sizeof(void *),"pixed");
      pixed_undototpix = 0;
      pixed_undototmem = 0;
   }
   
   if (pixed_undototmem > pixed_undomaxmem) 
   {
      zmessageACK(mWin,ZTX("Undo memory limit has been reached. \n"
                           "Save work with [done], then resume editing."));
      Mdrag = 0;
      return;
   }

   radius = pixed_radius;
   npix = 0;

   for (dy = -radius; dy <= radius; dy++)                                  //  count pixels in block
   for (dx = -radius; dx <= radius; dx++)
   {
      if (px + dx < 0 || px + dx > E3ww-1) continue;
      if (py + dy < 0 || py + dy > E3hh-1) continue;
      npix++;
   }
   
   ppixsave1 = (pixed_savepix *) zmalloc(npix*6+12,"pixed");               //  allocate memory for block
   pixed_undopixmem[pixed_undototpix] = ppixsave1;
   pixed_undototpix += 1;
   pixed_undototmem += npix * 6 + 12;
   
   ppixsave1->seq = pixed_undoseq;                                         //  save pixel block poop
   ppixsave1->npix = npix;
   ppixsave1->px = px;
   ppixsave1->py = py;
   ppixsave1->radius = radius;

   npix = 0;

   for (dy = -radius; dy <= radius; dy++)                                  //  save pixels in block
   for (dx = -radius; dx <= radius; dx++)
   {
      if (px + dx < 0 || px + dx > E3ww-1) continue;
      if (py + dy < 0 || py + dy > E3hh-1) continue;
      ppix3 = PXMpix(E3pxm16,(px+dx),(py+dy));                             //  edited image pixel
      ppixsave1->pixel[npix][0] = ppix3[0];
      ppixsave1->pixel[npix][1] = ppix3[1];
      ppixsave1->pixel[npix][2] = ppix3[2];
      npix++;
   }

   mempercent = int(100.0 * pixed_undototmem / pixed_undomaxmem);          //  update undo memory status
   if (mempercent != ppercent) {
      ppercent = mempercent;
      snprintf(undomemmessage,99,pixed_undomemmessage,mempercent,'%');
      zdialog_stuff(zd,"labmem",undomemmessage);
   }

   return;
}


//  undo last undo sequence number

void pixed_undo1()
{
   int            pindex, npix, radius, mempercent;
   int            ww, px, py, dx, dy;
   uint16         *ppix3;
   pixed_savepix  *ppixsave1;
   char           undomemmessage[100];
   zdialog        *zd = EFpixed.zd;
   
   pindex = pixed_undototpix;
   
   while (pindex > 0)
   {
      --pindex;
      ppixsave1 = pixed_undopixmem[pindex];
      if (ppixsave1->seq != pixed_undoseq) break;
      px = ppixsave1->px;
      py = ppixsave1->py;
      radius = ppixsave1->radius;

      npix = 0;
      for (dy = -radius; dy <= radius; dy++)
      for (dx = -radius; dx <= radius; dx++)
      {
         if (px + dx < 0 || px + dx > E3ww-1) continue;
         if (py + dy < 0 || py + dy > E3hh-1) continue;
         ppix3 = PXMpix(E3pxm16,(px+dx),(py+dy));
         ppix3[0] = ppixsave1->pixel[npix][0];
         ppix3[1] = ppixsave1->pixel[npix][1];
         ppix3[2] = ppixsave1->pixel[npix][2];
         npix++;
      }

      px = px - radius - 1;                                                //  v.10.12
      py = py - radius - 1;
      ww = 2 * radius + 3;
      mwpaint3(px,py,ww,ww);

      npix = ppixsave1->npix;
      zfree(ppixsave1);
      pixed_undopixmem[pindex] = 0;
      pixed_undototmem -= (npix * 6 + 12);
      --pixed_undototpix;
   }
   
   if (pixed_undoseq > 0) --pixed_undoseq;

   mempercent = int(100.0 * pixed_undototmem / pixed_undomaxmem);          //  update undo memory status
   snprintf(undomemmessage,99,pixed_undomemmessage,mempercent,'%');
   zdialog_stuff(zd,"labmem",undomemmessage);

   return;
}


//  free all undo memory

void pixed_freeundo()
{
   int            pindex;
   pixed_savepix  *ppixsave1;
   char           undomemmessage[100];
   zdialog        *zd = EFpixed.zd;

   pindex = pixed_undototpix;
   
   while (pindex > 0)
   {
      --pindex;
      ppixsave1 = pixed_undopixmem[pindex];
      zfree(ppixsave1);
   }
   
   if (pixed_undopixmem) zfree(pixed_undopixmem);
   pixed_undopixmem = 0;
   
   pixed_undoseq = 0;
   pixed_undototpix = 0;
   pixed_undototmem = 0;

   if (zd) {
      snprintf(undomemmessage,99,pixed_undomemmessage,0,'%');              //  undo memory = 0%
      zdialog_stuff(zd,"labmem",undomemmessage);
   }

   return;
}



