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

   Fotocx - edit photos and manage collections

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

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version. See https://www.gnu.org/licenses

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

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

   Fotocx image edit - Repair menu functions

   m_sharpen               sharpen an image
   m_blur                  blur an image
   m_blur_normal           mix pixels with immediate neighbors
   m_blur_radial           mix pixels in radial lines from chosen center
   m_blur_directed         blur locally in direction of mouse drag
   m_blur_graduated        blur relative to pixel contrast
   m_blur_background       blur relative to distance from foreground
   m_blur_motion           add motion blur to selected areas
   m_fix_motionblur        fix motion blur using Richardson-Lucy algorithm
   m_denoise               remove noise from an image
   m_defog                 add or remove fog/haze in an image or selected area
   m_redeyes               remove red-eyes from flash photos
   m_smart_erase           replace pixels inside selected areas with background
   m_remove_halo           remove halo effect caused by sharpen or other edits
   m_jpeg_artifacts        suppress JPEG compression artifacts
   m_anti_alias            fix jaggies on hard feature edges
   m_adjust_RGB            adjust brightness/color using RGB or CMY colors
   m_adjust_HSL            adjust color using HSL model
   m_remove_dust           remove dust specs from an image
   m_fringes               fix color fringes (chromatic aberration)

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

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

using namespace zfuncs;

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


//  image sharpen functions

namespace sharpen_names
{
   int      UM_radius, UM_amount, UM_thresh;
   int      AC_amount, AC_thresh;
   int      KH_radius;
   int      MD_radius, MD_dark, MD_light, *MD_britemap;
   float    RL_radius;
   int      RL_iters;
   ch       sharp_function[8] = "";

   int      brhood_radius;
   float    brhood_kernel[200][200];                                             //  up to radius = 99
   ch       brhood_method;                                                       //  g = gaussian, f = flat distribution
   float    *brhood_brightness = 0;                                              //  neighborhood brightness per pixel
   int      BRH_radius = 0;                                                      //  radius base for above calculation

   editfunc    EFsharp;
   ch          edit_hist[200];
}


//  menu function

void m_sharpen(GtkWidget *, ch *menu)
{
   using namespace sharpen_names;

   int    sharp_dialog_event(zdialog *zd, ch *event);
   void * sharp_thread(void *);
   int    ii;

   F1_help_topic = "sharpen";

   Plog(1,"m_sharpen \n");

   EFsharp.menuname = "Sharpen";
   EFsharp.menufunc = m_sharpen;
   EFsharp.Farea = 2;                                                            //  select area usable
   EFsharp.threadfunc = sharp_thread;                                            //  thread function
   EFsharp.Fpaintedits = 1;                                                      //  use with paint edits OK
   EFsharp.Fscript = 1;                                                          //  scripting supported

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

/***
          ____________________________________________
         |                 Sharpen                    |
         |                                            |
         |  [_] unsharp mask           radius  [__]   |
         |                             amount  [__]   |
         |                           threshold [__]   |
         |                                            |
         |  [_] amplify contrast       amount  [__]   |
         |                           threshold [__]   |
         |                                            |
         |  [_] Kuwahara               radius  [__]   |
         |                                            |
         |  [_] median diff            radius  [__]   |
         |                              dark   [__]   |
         |                              light  [__]   |
         |                                            |
         |  [_] Richardson-Lucy        radius  [__]   |
         |                          iterations [__]   |
         |                                            |
         |  [fix motion blur]                         |
         |                                            |
         |                   [Reset] [Apply] [OK] [X] |
         |____________________________________________|

***/

   zdialog *zd = zdialog_new("Sharpen",Mwin,"Reset","Apply","OK"," X ",null);
   EFsharp.zd = zd;

   zdialog_add_widget(zd,"hbox","hbum","dialog",0,"space=5");                    //  unsharp mask
   zdialog_add_widget(zd,"vbox","vb21","hbum",0,"space=2");
   zdialog_add_widget(zd,"label","space","hbum",0,"expand");
   zdialog_add_widget(zd,"vbox","vb22","hbum",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vb23","hbum",0,"homog|space=2");
   zdialog_add_widget(zd,"check","UM","vb21","unsharp mask","space=5");
   zdialog_add_widget(zd,"label","lab21","vb22","Radius");
   zdialog_add_widget(zd,"label","lab22","vb22","Amount");
   zdialog_add_widget(zd,"label","lab23","vb22","Threshold");
   zdialog_add_widget(zd,"zspin","radiusUM","vb23","1|20|1|2");
   zdialog_add_widget(zd,"zspin","amountUM","vb23","1|200|1|100");
   zdialog_add_widget(zd,"zspin","threshUM","vb23","1|100|1|0");

   zdialog_add_widget(zd,"hsep","sep3","dialog");                                //  amplify contrast
   zdialog_add_widget(zd,"hbox","hbgr","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb31","hbgr",0,"space=2");
   zdialog_add_widget(zd,"label","space","hbgr",0,"expand");
   zdialog_add_widget(zd,"vbox","vb32","hbgr",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vb33","hbgr",0,"homog|space=2");
   zdialog_add_widget(zd,"check","AC","vb31","amplify contrast","space=5");
   zdialog_add_widget(zd,"label","lab32","vb32","Amount");
   zdialog_add_widget(zd,"label","lab33","vb32","Threshold");
   zdialog_add_widget(zd,"zspin","amountAC","vb33","1|400|1|100");
   zdialog_add_widget(zd,"zspin","threshAC","vb33","1|100|1|0");

   zdialog_add_widget(zd,"hsep","sep4","dialog");                                //  kuwahara
   zdialog_add_widget(zd,"hbox","hbku","dialog",0,"space=5");
   zdialog_add_widget(zd,"check","KH","hbku","Kuwahara","space=3");
   zdialog_add_widget(zd,"label","space","hbku",0,"expand");
   zdialog_add_widget(zd,"label","lab42","hbku","Radius","space=3");
   zdialog_add_widget(zd,"zspin","radiusKH","hbku","1|9|1|1");

   zdialog_add_widget(zd,"hsep","sep5","dialog");                                //  median diff
   zdialog_add_widget(zd,"hbox","hbmd","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb51","hbmd",0,"space=2");
   zdialog_add_widget(zd,"label","space","hbmd",0,"expand");
   zdialog_add_widget(zd,"vbox","vb52","hbmd",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vb53","hbmd",0,"homog|space=2");
   zdialog_add_widget(zd,"check","MD","vb51","median diff","space=5");
   zdialog_add_widget(zd,"label","lab51","vb52","Radius");
   zdialog_add_widget(zd,"label","lab52","vb52","dark");
   zdialog_add_widget(zd,"label","lab53","vb52","light");
   zdialog_add_widget(zd,"zspin","radiusMD","vb53","1|10|1|3");
   zdialog_add_widget(zd,"zspin","darkMD","vb53","0|50|1|1");
   zdialog_add_widget(zd,"zspin","lightMD","vb53","0|50|1|1");

   zdialog_add_widget(zd,"hsep","sep6","dialog");                                //  Richardson-Lucy
   zdialog_add_widget(zd,"hbox","hbrl","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb61","hbrl",0,"space=2");
   zdialog_add_widget(zd,"label","space","hbrl",0,"expand");
   zdialog_add_widget(zd,"vbox","vb62","hbrl",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vb63","hbrl",0,"homog|space=2");
   zdialog_add_widget(zd,"check","RL","vb61","Richardson-Lucy","space=5");
   zdialog_add_widget(zd,"label","lab62","vb62","Radius");
   zdialog_add_widget(zd,"label","lab63","vb62","Iterations");
   zdialog_add_widget(zd,"zspin","radiusRL","vb63","1|9|0.1|1");
   zdialog_add_widget(zd,"zspin","itersRL","vb63","1|100|1|10");

   zdialog_add_widget(zd,"hsep","sep7","dialog",0,"space=3");                    //  fix motion blur
   zdialog_add_widget(zd,"hbox","hbmb","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","FMB","hbmb","fix motion blur","space=5");

   zdialog_load_inputs(zd);

   zdialog_fetch(zd,"UM",ii);                                                    //  set function from checkboxes
   if (ii) strcpy(sharp_function,"UM");
   zdialog_fetch(zd,"AC",ii);
   if (ii) strcpy(sharp_function,"AC");
   zdialog_fetch(zd,"KH",ii);
   if (ii) strcpy(sharp_function,"KH");
   zdialog_fetch(zd,"MD",ii);
   if (ii) strcpy(sharp_function,"MD");
   zdialog_fetch(zd,"RL",ii);
   if (ii) strcpy(sharp_function,"RL");

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

   return;
}


//  dialog event and completion callback function

int sharp_dialog_event(zdialog *zd, ch *event)                                   //  reworked for script files
{
   using namespace sharpen_names;

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

   zdialog_fetch(zd,"radiusUM",UM_radius);                                       //  get all parameters
   zdialog_fetch(zd,"amountUM",UM_amount);
   zdialog_fetch(zd,"threshUM",UM_thresh);
   zdialog_fetch(zd,"amountAC",AC_amount);
   zdialog_fetch(zd,"threshAC",AC_thresh);
   zdialog_fetch(zd,"radiusKH",KH_radius);
   zdialog_fetch(zd,"radiusMD",MD_radius);
   zdialog_fetch(zd,"radiusRL",RL_radius);
   zdialog_fetch(zd,"itersRL",RL_iters);
   zdialog_fetch(zd,"darkMD",MD_dark);
   zdialog_fetch(zd,"lightMD",MD_light);

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  [reset]
         zd->zstat = 0;
         edit_reset();
         return 1;
      }

      if (zd->zstat == 2) {                                                      //  [apply]
         zd->zstat = 0;
         edit_reset();

         if (*sharp_function) thread_signal();                                   //  start thread function
         else zmessageACK(Mwin,"no selection");                                  //  no choice made
         return 1;
      }

      if (zd->zstat == 3) {                                                      //  [OK]
         edit_addhist(edit_hist);                                                //  record edit history
         edit_done(0);                                                           //  done
         if (brhood_brightness) zfree(brhood_brightness);
         brhood_brightness = 0;
         return 1;
      }

      edit_cancel(0);                                                            //  discard edit
      if (brhood_brightness) zfree(brhood_brightness);
      brhood_brightness = 0;
      return 1;
   }

   if (strmatch(event,"paint")) thread_signal();

   if (strstr("UM AC KH MD RL",event))
   {
      zdialog_stuff(zd,"UM",0);                                                  //  make checkboxes like radio buttons
      zdialog_stuff(zd,"AC",0);
      zdialog_stuff(zd,"KH",0);
      zdialog_stuff(zd,"MD",0);
      zdialog_stuff(zd,"RL",0);
      zdialog_stuff(zd,event,1);
      strcpy(sharp_function,event);                                              //  set chosen method
   }

   if (strmatch(event,"FMB"))                                                    //  button - fix motion blur
      strcpy(sharp_function,"FMB");

   if (Fpaintedits && strstr("RL FMB",sharp_function))
   {
      zmessageACK(Mwin,"paint edits cannot be used for this method");
      sharp_dialog_event(zd,"UM");
      return 1;
   }

   if (strmatch(sharp_function,"FMB")) {                                         //  motion blur Richardson-Lucy algorithm
      edit_cancel(0);
      if (brhood_brightness) zfree(brhood_brightness);
      brhood_brightness = 0;
      m_fix_motionblur(0,0);
      return 1;
   }

   return 1;
}


//  sharpen image thread function

void * sharp_thread(void *)
{
   using namespace sharpen_names;

   int sharp_UM_thread(void);
   int sharp_AC_thread(void);
   int sharp_KH_thread(void);
   int sharp_MD_thread(void);
   int sharp_RL_thread(void);

   if (Fpaintedits && ! Mdrag) return 0;                                         //  if mouse paint, must be drag 

   if (strmatch(sharp_function,"UM")) sharp_UM_thread();
   if (strmatch(sharp_function,"AC")) sharp_AC_thread();
   if (strmatch(sharp_function,"KH")) sharp_KH_thread();
   if (strmatch(sharp_function,"MD")) sharp_MD_thread();
   if (strmatch(sharp_function,"RL")) sharp_RL_thread();

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

   if (! Fpaintedits) Fpaint2();                                                 //  update window
   return 0;
}


// ------------------------------------------------------------------------------

//  image sharpen function using unsharp mask

int sharp_UM_thread()
{
   using namespace sharpen_names;

   void  britehood(int radius, ch method);                                       //  compute neighborhood brightness
   void * sharp_UM_wthread(void *arg);

   int64      cc;

   if (! brhood_brightness) {
      cc = Eww * Ehh * sizeof(float);
      brhood_brightness = (float *) zmalloc(cc,"sharpen");
      BRH_radius = 0;
   }

   if (UM_radius != BRH_radius) {
      britehood(UM_radius,'f');                                                  //  re-calculate if radius changed
      BRH_radius = UM_radius;
   }

   if (sa_stat == sa_stat_fini) progress_setgoal(sa_Npixel);                     //  initz. progress counter
   else  progress_setgoal(Eww * Ehh);

   get_edit_pixels_init(NSMP,0);                                                 //  initz. pixel loop

   do_wthreads(sharp_UM_wthread,NSMP);                                           //  worker threads

   progress_setgoal(0);

   snprintf(edit_hist,200,"unsharp mask %d %d",UM_amount,UM_thresh);             //  record edit hist
   return 1;
}


void * sharp_UM_wthread(void *arg)                                               //  worker thread function
{
   using namespace sharpen_names;

   int         index = *((int *) arg);
   int         px, py, ii, Fend;
   float       amount, thresh, bright;
   float       mean, incr, ratio;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       *pix1, *pix3;
   float       blend;

   while (true)
   {
      if (Fescape) return 0;                                                     //  user kill

      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) return 0;
      if (blend == 0) continue;

      amount = 0.01 * UM_amount;                                                 //  0.0 to 2.0
      thresh = 0.4 * UM_thresh;                                                  //  0 to 40 (256 max. possible)

      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      bright = PIXBRIGHT(pix1);
      if (bright < 1) continue;                                                  //  effectively black
      ii = py * Eww + px;
      mean = brhood_brightness[ii];

      incr = (bright - mean);
      if (fabsf(incr) < thresh) continue;                                        //  omit low-contrast pixels

      incr = incr * amount;                                                      //  0.0 to 2.0
      if (bright + incr > 255) incr = 255 - bright;
      ratio = (bright + incr) / bright;
      if (ratio < 0) ratio = 0;

      R1 = R9 = pix1[0];                                                         //  input RGB
      G1 = G9 = pix1[1];
      B1 = B9 = pix1[2];

      R3 = pix3[0];                                                              //  current output image RGB
      G3 = pix3[1];
      B3 = pix3[2];

      R9 = ratio * R1;                                                           //  new output RGB
      G9 = ratio * G1;
      B9 = ratio * B1;

      RGBfix(R9,G9,B9);

      if (Fpaintedits)                                                           //  gradual edit within mouse circle
      {
         if (blend > 0)
         {
            R3 = blend * R9 + (1-blend) * R3;                                    //  increase edit
            G3 = blend * G9 + (1-blend) * G3;
            B3 = blend * B9 + (1-blend) * B3;
         }

         else if (blend < 0)
         {
            R3 = -blend * R1 + (1+blend) * R3;                                   //  decrease edit
            G3 = -blend * G1 + (1+blend) * G3;
            B3 = -blend * B1 + (1+blend) * B3;
         }
      }

      else                                                                       //  full edit for image or area
      {
         R3 = blend * R9 + (1-blend) * R1;
         G3 = blend * G9 + (1-blend) * G1;
         B3 = blend * B9 + (1-blend) * B1;
      }

      pix3[0] = R3;
      pix3[1] = G3;
      pix3[2] = B3;

      progress_addvalue(index,1);                                                //  track progress
   }

   return 0;
}


// ------------------------------------------------------------------------------

//  sharpen image by amplifying existing contrast

int sharp_AC_thread()
{
   using namespace sharpen_names;

   void * sharp_AC_wthread(void *arg);

   if (sa_stat == sa_stat_fini) progress_setgoal(sa_Npixel);                     //  initz. progress counter
   else  progress_setgoal(Eww * Ehh);

   get_edit_pixels_init(NSMP,1);                                                 //  initz. pixel loop

   do_wthreads(sharp_AC_wthread,NSMP);                                           //  worker threads

   progress_setgoal(0);

   snprintf(edit_hist,200,"amplifycon %d %d",AC_amount,AC_thresh);                //  record edit hist
   return 1;
}


void * sharp_AC_wthread(void *arg)                                               //  worker thread function
{
   using namespace sharpen_names;

   float       *pix1, *pix3;
   int         px, py, Fend;
   int         nc = E1pxm->nc;
   float       blend, amount, thresh;
   float       b1, b1x, b1y, b3x, b3y, b3, bf, f1, f2;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;

   int         index = *((int *) arg);

   amount = 1 + 0.01 * AC_amount;                                                //  1.0 - 5.0
   thresh = AC_thresh;                                                           //  0 - 100

   while (true)
   {
      if (Fescape) return 0;                                                     //  user kill

      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) return 0;
      if (blend == 0) continue;

      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      b1 = PIXBRIGHT(pix1);                                                      //  pixel brightness, 0 - 256
      if (b1 == 0) continue;                                                     //  black, don't change
      b1x = b1 - PIXBRIGHT(pix1-nc);                                             //  horiz. brightness gradient
      b1y = b1 - PIXBRIGHT(pix1-nc * Eww);                                       //  vertical
      f1 = fabsf(b1x) + fabsf(b1y);

      if (f1 < thresh)                                                           //  moderate brightness change for
         f1 = f1 / thresh;                                                       //    pixels below threshold gradient
      else  f1 = 1.0;
      f2 = 1.0 - f1;

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

      b3x = PIXBRIGHT(pix1-nc) + b1x;                                            //  + prior pixel brightness
      b3y = PIXBRIGHT(pix1-nc * Eww) + b1y;                                      //  = new brightness
      b3 = 0.5 * (b3x + b3y);

      b3 = f1 * b3 + f2 * b1;                                                    //  possibly moderated

      bf = b3 / b1;                                                              //  ratio of brightness change
      if (bf < 0) bf = 0;
      if (bf > 4) bf = 4;

      R1 = R9 = pix1[0];                                                         //  input RGB
      G1 = G9 = pix1[1];
      B1 = B9 = pix1[2];

      R3 = pix3[0];                                                              //  current output RGB
      G3 = pix3[1];
      B3 = pix3[2];

      R9 = bf * R1;                                                              //  new output RGB
      G9 = bf * G1;
      B9 = bf * B1;

      RGBfix(R9,G9,B9);

      if (Fpaintedits)                                                           //  gradual edit within mouse circle
      {
         if (blend > 0)
         {
            R3 = blend * R9 + (1-blend) * R3;                                    //  increase edit
            G3 = blend * G9 + (1-blend) * G3;
            B3 = blend * B9 + (1-blend) * B3;
         }

         else if (blend < 0)
         {
            R3 = -blend * R1 + (1+blend) * R3;                                   //  decrease edit
            G3 = -blend * G1 + (1+blend) * G3;
            B3 = -blend * B1 + (1+blend) * B3;
         }
      }

      else                                                                       //  full edit for image or area
      {
         R3 = blend * R9 + (1-blend) * R1;
         G3 = blend * G9 + (1-blend) * G1;
         B3 = blend * B9 + (1-blend) * B1;
      }

      pix3[0] = R3;
      pix3[1] = G3;
      pix3[2] = B3;

      progress_addvalue(index,1);                                                //  track progress
   }

   return 0;
}


//  callable sharp_AC() used by rotate function
//  returns E3 = sharpened E3

void sharp_AC(int amount, int thresh)
{
   using namespace sharpen_names;
   
   int sharp_AC_thread(void); 

   PXM *PXMtemp = E1pxm;                                                         //  save E1
   E1pxm = PXM_copy(E3pxm);                                                      //  copy E3 > E1
   AC_amount = amount;
   AC_thresh = thresh;
   sharp_AC_thread();                                                            //  E3 = sharpened E1
   PXM_free(E1pxm);
   E1pxm = PXMtemp;                                                              //  restore org. E1
   return;
}


// ------------------------------------------------------------------------------

//  sharpen edges using the Kuwahara algorithm

int sharp_KH_thread()
{
   using namespace sharpen_names;

   void * sharp_KH_wthread(void *arg);

   if (sa_stat == sa_stat_fini) progress_setgoal(sa_Npixel);                     //  initz. progress counter
   else  progress_setgoal(Eww * Ehh);

   get_edit_pixels_init(NSMP,KH_radius);                                         //  initz. pixel loop

   do_wthreads(sharp_KH_wthread,NSMP);                                           //  worker threads

   progress_setgoal(0);

   snprintf(edit_hist,200,"kuwahara %d",KH_radius);                              //  record edit hist
   return 1;
}


void * sharp_KH_wthread(void *arg)                                               //  worker thread function
{
   using namespace sharpen_names;

   float       *pix1, *pix3;
   int         px, py, qx, qy, rx, ry;
   int         rad, N, Fend;
   float       red, green, blue, red2, green2, blue2;
   float       vmin, vall, vred, vgreen, vblue;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       blend;

   int      index = *((int *) arg);

   rad = KH_radius;                                                              //  user input radius
   N = (rad + 1) * (rad + 1);

   while (true)
   {
      if (Fescape) return 0;                                                     //  user kill 

      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) return 0;
      if (blend == 0) continue;

      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      R1 = R9 = pix1[0];                                                         //  input image RGB values
      G1 = G9 = pix1[1];
      B1 = B9 = pix1[2];

      R3 = pix3[0];                                                              //  current output image RGB
      G3 = pix3[1];
      B3 = pix3[2];

      vmin = 99999;
      R9 = G9 = B9 = 0;

      for (qy = py - rad; qy <= py; qy++)                                        //  loop all surrounding neighborhoods
      for (qx = px - rad; qx <= px; qx++)
      {
         red = green = blue = 0;
         red2 = green2 = blue2 = 0;

         for (ry = qy; ry <= qy + rad; ry++)                                     //  loop all pixels in neighborhood
         for (rx = qx; rx <= qx + rad; rx++)
         {
            pix1 = PXMpix(E1pxm,rx,ry);
            red += pix1[0];                                                      //  compute mean RGB and mean RGB**2
            red2 += pix1[0] * pix1[0];
            green += pix1[1];
            green2 += pix1[1] * pix1[1];
            blue += pix1[2];
            blue2 += pix1[2] * pix1[2];
         }

         red = red / N;                                                          //  mean RGB of neighborhood
         green = green / N;
         blue = blue / N;

         vred = red2 / N - red * red;                                            //  variance RGB
         vgreen = green2 / N - green * green;
         vblue = blue2 / N - blue * blue;

         vall = vred + vgreen + vblue;                                           //  save RGB values with least variance
         if (vall < vmin) {
            vmin = vall;
            R9 = red;
            G9 = green;
            B9 = blue;
         }
      }

      if (Fpaintedits)                                                           //  gradual edit within mouse circle
      {
         if (blend > 0)
         {
            R3 = blend * R9 + (1-blend) * R3;                                    //  increase edit
            G3 = blend * G9 + (1-blend) * G3;
            B3 = blend * B9 + (1-blend) * B3;
         }

         else if (blend < 0)
         {
            R3 = -blend * R1 + (1+blend) * R3;                                   //  decrease edit
            G3 = -blend * G1 + (1+blend) * G3;
            B3 = -blend * B1 + (1+blend) * B3;
         }
      }

      else                                                                       //  full edit for image or area
      {
         R3 = blend * R9 + (1-blend) * R1;
         G3 = blend * G9 + (1-blend) * G1;
         B3 = blend * B9 + (1-blend) * B1;
      }

      pix3[0] = R3;                                                              //  set output pixel
      pix3[1] = G3;
      pix3[2] = B3;

      progress_addvalue(index,1);                                                //  track progress
   }

   return 0;
}


// ------------------------------------------------------------------------------

//  sharpen edges using the median difference algorithm

int sharp_MD_thread()
{
   using namespace sharpen_names;

   void * sharp_MD_wthread(void *arg);

   int      px, py, ii;
   float    *pix1;
   
   MD_britemap = (int *) zmalloc(Eww * Ehh * sizeof(int),"sharpen");

   for (py = 0; py < Ehh; py++)                                                  //  loop all pixels
   for (px = 0; px < Eww; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);                                                //  initz. pixel brightness map
      ii = py * Eww + px;
      MD_britemap[ii] = PIXBRIGHT(pix1);
   }

   if (sa_stat == sa_stat_fini) progress_setgoal(sa_Npixel);                     //  initz. progress counter
   else  progress_setgoal(Eww * Ehh);

   get_edit_pixels_init(NSMP,MD_radius);                                         //  initz. pixel loop

   do_wthreads(sharp_MD_wthread,NSMP);

   progress_setgoal(0);

   zfree(MD_britemap);

   snprintf(edit_hist,200,"median diff %d %d %d",MD_radius,MD_dark,MD_light);    //  record edit hist
   return 1;
}


void * sharp_MD_wthread(void *arg)                                               //  worker thread function
{
   using namespace sharpen_names;
   
   int         index = *((int *) arg);
   int         rad, dark, light, *britemap;
   int         ii, px, py, Fend;
   int         dy, dx, ns;
   float       R1, G1, B1, R2, G2, B2, R3, G3, B3, R9, G9, B9;
   float       F, blend;
   float       *pix1, *pix3;
   int         bright, median;
   int         bsortN[1681];                                                     //  radius <= 20 (41 x 41 pixels)

   rad = MD_radius;                                                              //  parameters from dialog
   dark = MD_dark;
   light = MD_light;
   britemap = MD_britemap;

   while (true)
   {
      if (Fescape) return 0;                                                     //  user kill 

      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) return 0;
      if (blend == 0) continue;

      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      R1 = R9 = pix1[0];                                                         //  input image RGB values
      G1 = G9 = pix1[1];
      B1 = B9 = pix1[2];

      R3 = pix3[0];                                                              //  current output image RGB
      G3 = pix3[1];
      B3 = pix3[2];

      ns = 0;

      for (dy = py-rad; dy <= py+rad; dy++)                                      //  loop surrounding pixels
      for (dx = px-rad; dx <= px+rad; dx++)                                      //  get brightness values
      {
         ii = dy * Eww + dx;
         bsortN[ns] = britemap[ii];
         ns++;
      }

      heapsort(bsortN,ns);                                                       //  sort the pixels (single thread)       24.60
      median = bsortN[ns/2];                                                     //  median brightness

      bright = PIXBRIGHT(pix3);

      if (bright < median) {
         F = 1.0 - 0.1 * dark * (median - bright) / (median + 50);
         R2 = R1 * F;
         G2 = G1 * F;
         B2 = B1 * F;
         if (R2 > 0 && G2 > 0 && B2 > 0) {
            R9 = R2;
            G9 = G2;
            B9 = B2;
         }
      }

      if (bright > median) {
         F = 1.0 + 0.03 * light * (bright - median) / (median + 50);
         R2 = R1 * F;
         G2 = G1 * F;
         B2 = B1 * F;
         if (R2 < 255 && G2 < 255 && B2 < 255) {
            R9 = R2;
            G9 = G2;
            B9 = B2;
         }
      }

      if (Fpaintedits)                                                           //  gradual edit within mouse circle
      {
         if (blend > 0)
         {
            R3 = blend * R9 + (1-blend) * R3;                                    //  increase edit
            G3 = blend * G9 + (1-blend) * G3;
            B3 = blend * B9 + (1-blend) * B3;
         }

         else if (blend < 0)
         {
            R3 = -blend * R1 + (1+blend) * R3;                                   //  decrease edit
            G3 = -blend * G1 + (1+blend) * G3;
            B3 = -blend * B1 + (1+blend) * B3;
         }
      }

      else                                                                       //  full edit for image or area
      {
         R3 = blend * R9 + (1-blend) * R1;
         G3 = blend * G9 + (1-blend) * G1;
         B3 = blend * B9 + (1-blend) * B1;
      }

      pix3[0] = R3;                                                              //  set output pixel
      pix3[1] = G3;
      pix3[2] = B3;

      progress_addvalue(index,1);                                                //  track progress
   }

   return 0;
}


// ------------------------------------------------------------------------------

//  Sharpen image using the Richardson-Lucy deconvolution algorithm
//  (best for camera out-of-focus area - point source blur disc is uniform)

int sharp_RL_thread()
{
   using namespace sharpen_names;

   void * sharp_RL_wthread(void *arg);

   do_wthreads(sharp_RL_wthread,3);                                              //  3 threads for 3 RGB colors
   snprintf(edit_hist,200,"Richardson-Lucy %.1f %d",RL_radius,RL_iters);         //  record edit hist

   return 1;
}


void * sharp_RL_wthread(void *arg)                                               //  worker thread function
{
   using namespace sharpen_names;

   void RLdecon(PXM *pxm, int rgb, float frad, int iter);

   int   rgb = *((int *) arg);

   progress_setgoal(6 * RL_iters);

   RLdecon(E3pxm,rgb,RL_radius,RL_iters);

   progress_setgoal(0);

   return 0;
}


//  Deblur out-of-focus image using Richardson-Lucy deconvolution
//    pxm     input image in PXM format (float RGB values)
//    rgb     RGB channel to deblur (0/1/2)
//    frad    input blur disc radius (found by trial and error)
//    iter    R-L algorithm iterations to use
//  Code below follows notation in Richardson-Lucy Wikipedia topic.

void RLdecon(PXM *pxm, int rgb, float frad, int iter)
{
   using namespace sharpen_names;

   int      ww, hh;                                                              //  image dimensions
   int      px, py, rx, ry, sx, sy;
   int      pxlo, pxhi, pylo, pyhi;
   int      ii, jj, marg, dist = 0;

   float    *pix;                                                                //  pixel within input image
   float    *Di;                                                                 //  initial pixel values (blurred)
   float    *Uj;                                                                 //  current pixel values (deblurred
   float    *Ci;                                                                 //  R-L factor
   float    Pij;                                                                 //  R-L factor
   float    f1, f2;

   float    PSF[20][20];                                                         //  1x blur disc, radius < 10
   float    PSF2[200][200];                                                      //  9x blur disc, radius < 100
   int      irad, irad2, B, B2;
   float    frad2, R, S;

   ww = pxm->ww;                                                                 //  image dimensions
   hh = pxm->hh;

   Di = (float *) malloc(ww * hh * sizeof(float));                               //  allocate memory
   Uj = (float *) malloc(ww * hh * sizeof(float));
   Ci = (float *) malloc(ww * hh * sizeof(float));

//  compute 9x blur disc (point spread function PSF)
//  shrink to 1x size for higher precision

   irad = frad + 0.5;                                                            //  disc radius, next greater integer
   B = 2 * irad + 1;                                                             //  disc container array, B x B
   B2 = 9 * B;                                                                   //  9x container size
   irad2 = B2 / 2;                                                               //  container irad
   frad2 = 9 * frad;                                                             //  container frad

   for (ry = -irad2; ry <= +irad2; ry++)                                         //  build 9x disc
   for (rx = -irad2; rx <= +irad2; rx++)
   {
      R = sqrtf(rx * rx + ry * ry);
      if (R > frad2 + 0.5)                                                       //  pixel fully outside radius
         PSF2[99+rx][99+ry] = 0;
      else if (R < frad2 - 0.5)                                                  //  pixel fully inside radius
         PSF2[99+rx][99+ry] = 1.0;
      else                                                                       //  pixel strides radius
         PSF2[99+rx][99+ry] = frad2 + 0.5 - R;
   }

   for (ry = -irad; ry <= +irad; ry++)                                           //  loop 1x disc
   for (rx = -irad; rx <= +irad; rx++)
      PSF[9+rx][9+ry] = 0;                                                       //  clear to zeros

   for (ry = -irad2; ry <= +irad2; ry++)                                         //  loop 9x disc
   for (rx = -irad2; rx <= +irad2; rx++)
   {
      sx = (rx + irad2) / 9 - irad;                                              //  corresp. 1x disc cell
      sy = (ry + irad2) / 9 - irad;
      PSF[9+sx][9+sy] += PSF2[99+rx][99+ry];                                     //  aggregate 9x disc into 1x disc
   }

   S = 0;
   for (ry = -irad; ry <= +irad; ry++)                                           //  loop pixels in 1x disc
   for (rx = -irad; rx <= +irad; rx++)
      S += PSF[9+rx][9+ry];                                                      //  sum pixel values

   for (ry = -irad; ry <= +irad; ry++)                                           //  loop pixels in 1x disc
   for (rx = -irad; rx <= +irad; rx++)
      PSF[9+rx][9+ry] /= S;                                                      //  normalize to sum 1.0

/***  print blur disc

   if (rgb == 0) {
      printf("\nfrad: %.1f  irad: %d \n",frad,irad);
      for (ry = -irad; ry <= +irad; ry++) {
         for (rx = -irad; rx <= +irad; rx++)
            printf("%8.3f",PSF[9+rx][9+ry]);
         printf("\n");
      }
      printf("\n");
   }

***/

//  initialize Di and Uj from input image rgb values

   marg = 5.3 * irad + 0.5 * iter;                                               //  invalid pixels - empirical

   pxlo = irad;                                                                  //  avoid image edges
   pxhi = ww - irad;
   pylo = irad;
   pyhi = hh - irad;

   if (sa_stat == sa_stat_fini)                                                  //  if select area, reduce processing
   {
      pxlo = sa_minx - marg;
      pxhi = sa_maxx + marg;
      pylo = sa_miny - marg;
      pyhi = sa_maxy + marg;

      if (pxlo < irad) pxlo = irad;
      if (pxhi > ww-irad) pxhi = ww - irad;
      if (pylo < irad) pylo = irad;
      if (pyhi > hh-irad) pyhi = hh - irad;
   }

   for (py = pylo; py < pyhi; py++)
   for (px = pxlo; px < pxhi; px++)
   {
      if (Fescape) break;                                                        //  user kill 
      pix = PXMpix(pxm,px,py);
      ii = py * ww + px;
      Di[ii] = Uj[ii] = pix[rgb];
   }

//  loop algorithm iterations

   for (int tt = 0; tt < iter; tt++)
   {
      if (Fescape) break;                                                        //  user kill 

//  compute Ci for each pixel i

      for (py = pylo; py < pyhi; py++)                                           //  loop all pixels i
      for (px = pxlo; px < pxhi; px++)
      {
         ii = py * ww + px;

         Ci[ii] = 0;
         for (ry = -irad; ry <= +irad; ry++)                                     //  loop contributing pixels j
         for (rx = -irad; rx <= +irad; rx++) {
            Pij = PSF[9+rx][9+ry];                                               //  contribution factor
            jj = ii + ry * ww + rx;
            Ci[ii] += Pij * Uj[jj];                                              //  aggregate
         }

         if (Ci[ii] <= 0) Ci[ii] = 1;
         if (isnan(Ci[ii])) Ci[ii] = 1;
      }

      progress_addvalue(rgb,1);                                                  //  track progress (rgb = thread)

//  compute new Uj for each pixel j

      for (py = pylo; py < pyhi; py++)                                           //  loop all pixels j
      for (px = pxlo; px < pxhi; px++)
      {
         jj = py * ww + px;

         S = 0;
         for (ry = -irad; ry <= +irad; ry++)                                     //  loop all pixels i that pixel j
         for (rx = -irad; rx <= +irad; rx++) {                                   //    contributes to
            ii = jj + ry * ww + rx;
            Pij = PSF[9+rx][9+ry];                                               //  contribution factor
            S += Di[ii] / Ci[ii] * Pij;                                          //  Di / Ci * Pij
         }

         Uj[jj] = Uj[jj] * S;                                                    //  new Uj
      }

      progress_addvalue(rgb,1);                                                  //  track progress (rgb = thread)
   }

   for (py = pylo+marg; py < pyhi-marg; py++)                                    //  replace input image rgb values
   for (px = pxlo+marg; px < pxhi-marg; px++)                                    //  avoid margins with invalid pixels
   {
      if (Fescape) break;                                                        //  user kill

      ii = py * ww + px;

      if (sa_stat == sa_stat_fini) {                                             //  select area active
         dist = sa_pixmap[ii];
         if (! dist) continue;                                                   //  pixel outside area
      }

      if (isnan(Uj[ii])) Uj[ii] = 0;
      if (Uj[ii] < 0) Uj[ii] = 0;                                                //  modified pixel
      if (Uj[ii] > 255) Uj[ii] = 255;

      pix = PXMpix(pxm,px,py);                                                   //  output image pixel
      f1 = 1.0;
      f2 = 0.0;

      pix[rgb] = f1 * Uj[ii] + f2 * Di[ii];
   }

   free(Uj);
   free(Di);
   free(Ci);

   return;
}


// ------------------------------------------------------------------------------

//  Compute the mean brightness of all pixel neighborhoods,
//  using a Gaussian or a flat distribution for the weightings.
//  If a select area is active, only inside pixels are calculated.
//  The flat method is 10-100x faster than the Gaussian method.

void britehood(int radius, ch method)
{
   using namespace sharpen_names;

   void * brhood_wthread(void *arg);

   int      rad, radflat2, dx, dy;
   float    kern;

   brhood_radius = radius;
   brhood_method = method;

   if (brhood_method == 'g')                                                     //  compute Gaussian kernel
   {                                                                             //  (not currently used)
      rad = brhood_radius;
      radflat2 = rad * rad;

      for (dy = -rad; dy <= rad; dy++)
      for (dx = -rad; dx <= rad; dx++)
      {
         if (dx*dx + dy*dy <= radflat2)                                          //  cells within radius
            kern = exp( - (dx*dx + dy*dy) / radflat2);
         else kern = 0;                                                          //  outside radius
         brhood_kernel[dy+rad][dx+rad] = kern;
      }
   }

   if (sa_stat == sa_stat_fini) progress_setgoal(sa_Npixel);                     //  initz. progress counter
   else  progress_setgoal(Eww * Ehh);

   do_wthreads(brhood_wthread,NSMP);                                             //  worker threads

   progress_setgoal(0);
   return;
}


//  worker thread function

void * brhood_wthread(void *arg)
{
   using namespace sharpen_names;

   int      index = *((int *) arg);
   int      rad = brhood_radius;
   int      ii, px, py, qx, qy, Fstart;
   float    kern, bsum, bsamp, bmean;
   float    *pixel;

   if (brhood_method == 'g')                                                     //  use round gaussian distribution
   {
      for (py = index; py < Ehh; py += NSMP)
      for (px = 0; px < Eww; px++)
      {
         if (Fescape) return 0;                                                  //  user kill

         if (sa_stat == sa_stat_fini) {                                          //  select area,
            ii = py * Eww + px;                                                  //    use only inside pixels
            if (! sa_pixmap[ii]) continue;
         }

         bsum = bsamp = 0;

         for (qy = py-rad; qy <= py+rad; qy++)                                   //  computed weighted sum of brightness
         for (qx = px-rad; qx <= px+rad; qx++)                                   //    for pixels in neighborhood
         {
            if (qy < 0 || qy > Ehh-1) continue;
            if (qx < 0 || qx > Eww-1) continue;
            kern = brhood_kernel[qy+rad-py][qx+rad-px];
            pixel = PXMpix(E1pxm,qx,qy);
            bsum += PIXBRIGHT(pixel) * kern;                                     //  sum brightness * weight
            bsamp += kern;                                                       //  sum weights
         }

         bmean = bsum / bsamp;                                                   //  mean brightness
         ii = py * Eww + px;
         brhood_brightness[ii] = bmean;                                          //  pixel value

         progress_addvalue(index,1);                                             //  track progress
      }
   }

   if (brhood_method == 'f')                                                     //  use square flat distribution
   {
      Fstart = 1;
      bsum = bsamp = 0;

      for (py = index; py < Ehh; py += NSMP)
      for (px = 0; px < Eww; px++)
      {
         if (Fescape) return 0;                                                  //  user kill

         if (sa_stat == sa_stat_fini) {                                          //  select area,
            ii = py * Eww + px;                                                  //     compute only inside pixels
            if (! sa_pixmap[ii]) {
               Fstart = 1;
               continue;
            }
         }

         if (px == 0) Fstart = 1;

         if (Fstart)
         {
            Fstart = 0;
            bsum = bsamp = 0;

            for (qy = py-rad; qy <= py+rad; qy++)                                //  add up all columns
            for (qx = px-rad; qx <= px+rad; qx++)
            {
               if (qy < 0 || qy > Ehh-1) continue;
               if (qx < 0 || qx > Eww-1) continue;
               pixel = PXMpix(E1pxm,qx,qy);
               bsum += PIXBRIGHT(pixel);
               bsamp += 1;
            }
         }
         else
         {
            qx = px-rad-1;                                                       //  subtract first-1 column
            if (qx >= 0) {
               for (qy = py-rad; qy <= py+rad; qy++)
               {
                  if (qy < 0 || qy > Ehh-1) continue;
                  pixel = PXMpix(E1pxm,qx,qy);
                  bsum -= PIXBRIGHT(pixel);
                  bsamp -= 1;
               }
            }
            qx = px+rad;                                                         //  add last column
            if (qx < Eww) {
               for (qy = py-rad; qy <= py+rad; qy++)
               {
                  if (qy < 0 || qy > Ehh-1) continue;
                  pixel = PXMpix(E1pxm,qx,qy);
                  bsum += PIXBRIGHT(pixel);
                  bsamp += 1;
               }
            }
         }

         bmean = bsum / bsamp;                                                   //  mean brightness
         ii = py * Eww + px;
         brhood_brightness[ii] = bmean;

         progress_addvalue(index,1);                                             //  track progress
      }
   }

   return 0;
}


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

//  blur image steering function - choose from multiple blur functions

void m_blur(GtkWidget *, ch *menu)
{
   int  blur_dialog_event(zdialog *zd, ch *event);

   zdialog  *zd;

   F1_help_topic = "blur";

   Plog(1,"m_blur \n");

/***
       ____________________________________________________________________
      |                     Blur Image                                     |
      |                                                                    |
      |  [_] Normal Blur - mix pixels with all neighbor pixels             |
      |  [_] Radial Blur - mix pixels in radial lines from chosen center   |
      |  [_] Directed Blur - blur locally in direction of mouse drag       |
      |  [_] Graduated Blur - blur relative to pixel contrast              |
      |  [_] Background Blur - blur relative to distance from foreground   |
      |  [_] Motion Blur - add motion blur to selected areas               |
      |                                                                    |
      |                                                                [X] |
      |____________________________________________________________________|

***/

   zd = zdialog_new("Blur Image",Mwin," X ",null);
   zdialog_add_widget(zd,"check","normal","dialog","Normal Blur - mix pixels with all neighbor pixels");
   zdialog_add_widget(zd,"check","radial","dialog","Radial Blur - mix pixels in radial lines from chosen center");
   zdialog_add_widget(zd,"check","directed","dialog","Directed Blur - blur locally in direction of mouse drag");
   zdialog_add_widget(zd,"check","graduated","dialog","Graduated Blur - blur relative to pixel contrast");
   zdialog_add_widget(zd,"check","background","dialog","Background Blur - blur relative to distance from foreground");
   zdialog_add_widget(zd,"check","motion","dialog","Motion Blur - add motion blur to selected areas");

   zdialog_stuff(zd,"normal",0);
   zdialog_stuff(zd,"radial",0);
   zdialog_stuff(zd,"directed",0);
   zdialog_stuff(zd,"graduated",0);
   zdialog_stuff(zd,"background",0);
   zdialog_stuff(zd,"motion",0);

   zdialog_run(zd,blur_dialog_event,"save");
   return;
}


//  dialog event and completion function

int blur_dialog_event(zdialog *zd, ch *event)
{
   char  event2[20];
   
   void m_blur_normal(GtkWidget *, ch *menu);
   void m_blur_radial(GtkWidget *, ch *menu);
   void m_blur_directed(GtkWidget *, ch *menu);
   void m_blur_graduated(GtkWidget *, ch *menu);
   void m_blur_background(GtkWidget *, ch *menu);
   void m_blur_motion(GtkWidget *, ch *menu);

   if (zd->zstat) {
      zdialog_free(zd);
      return 1;
   }

   if (zstrstr("normal radial directed graduated background motion",event)) {
      strncpy0(event2,event,20);
      zdialog_free(zd);
   }

   if (strmatch(event2,"normal")) m_blur_normal(0,0);
   if (strmatch(event2,"radial")) m_blur_radial(0,0);
   if (strmatch(event2,"directed")) m_blur_directed(0,0);
   if (strmatch(event2,"graduated")) m_blur_graduated(0,0);
   if (strmatch(event2,"background")) m_blur_background(0,0);
   if (strmatch(event2,"motion")) m_blur_motion(0,0);

   return 1;
}


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

//  normal blur

namespace blur_normal_names
{
   editfunc    EFblur;
   PXM         *E2pxm;
   float       blur_radius;                                                      //  blur radius
   float       blur_weight[1000];                                                //  blur radius limit 999
}


void m_blur_normal(GtkWidget *, ch *menu)
{
   using namespace blur_normal_names;

   int  blur_normal_dialog_event(zdialog *zd, ch *event);
   void * blur_normal_thread(void *);

   F1_help_topic = "blur";

   Plog(1,"m_blur_normal \n");

   EFblur.menuname = "Blur Normal";
   EFblur.menufunc = m_blur_normal;
   EFblur.Farea = 2;                                                             //  select area usable
   EFblur.threadfunc = blur_normal_thread;                                       //  thread function
   EFblur.Fpaintedits = 1;                                                       //  allow paint edits

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

/***
          _________________________________________
         |            Normal Blur                  |
         |                                         |
         |  Blur Radius [____]                     |
         |                                         |
         |                [Reset] [Apply] [OK] [X] |
         |_________________________________________|

***/

   zdialog *zd = zdialog_new("Normal Blur",Mwin,"Reset","Apply","OK"," X ",null);
   EFblur.zd = zd;

   zdialog_add_widget(zd,"hbox","hbnb","dialog");
   zdialog_add_widget(zd,"label","labrad","hbnb","Radius","space=5");
   zdialog_add_widget(zd,"zspin","blur_radius","hbnb","1|999|1|10","space=5|size=3");

   E2pxm = 0;
   blur_radius = 10;

   zdialog_load_inputs(zd);
   zdialog_run(zd,blur_normal_dialog_event,"save");                              //  run dialog

   return;
}


//  dialog event and completion callback function

int blur_normal_dialog_event(zdialog * zd, ch *event)
{
   using namespace blur_normal_names;

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

   zdialog_fetch(zd,"blur_radius",blur_radius);

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  [reset]
      {
         zd->zstat = 0;                                                          //  keep dialog active
         edit_reset();
         return 1;
      }

      if (zd->zstat == 2)                                                        //  [apply]
      {
         zd->zstat = 0;                                                          //  keep dialog active
         thread_signal();                                                        //  trigger thread
         return 1;
      }

      if (zd->zstat == 3) {                                                      //  [OK]
         edit_addhist("rad:%.0f",blur_radius);
         edit_done(0);
      }

      else edit_cancel(0);                                                       //  cancel

      if (E2pxm) PXM_free(E2pxm);                                                //  free memory
      E2pxm = 0;

      return 1;
   }
   
   if (strmatch(event,"paint"))                                                  //  from paint edits 
      thread_signal();

   return 1;
}


//  thread function

void * blur_normal_thread(void *)
{
   using namespace blur_normal_names;

   void * blur_normal_wthread1(void *);
   void * blur_normal_wthread2(void *);

   int         ii;
   float       rad, w, wsum;

   if (Fpaintedits && ! Mdrag) return 0;                                         //  if mouse paint, must be drag

   rad = blur_radius;
   wsum = 0;

   for (ii = 0; ii <= rad; ii++)                                                 //  set pixel weight per distance
   {                                                                             //      example, rad = 10
      w = 1.0 - (ii + 0.25) / rad;                                               //  dist:    0   1   2   3   5   7   9
      w = w * w;                                                                 //  weight: .95 .77 .60 .46 .23 .08 .01
      blur_weight[ii] = w;
      wsum += w;
   }

   for (ii = 0; ii <= rad; ii++)                                                 //  make weights sum to 1.0
      blur_weight[ii] = blur_weight[ii] / wsum;

   if (E2pxm) PXM_free(E2pxm);
   E2pxm = PXM_copy(E3pxm);                                                      //  intermediate image

   if (! Fpaintedits) 
   {
      if (sa_stat == sa_stat_fini) progress_setgoal(sa_Npixel * 2);              //  initz. progress counter
      else  progress_setgoal(Eww * Ehh * 2);
   }

   get_edit_pixels_init(NSMP,0);
   do_wthreads(blur_normal_wthread1,NSMP);                                       //  worker threads 1

   get_edit_pixels_init(NSMP,0);
   do_wthreads(blur_normal_wthread2,NSMP);                                       //  worker threads 2

   progress_setgoal(0);

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

   if (! Fpaintedits) Fpaint2();

   return 0;
}


//  worker thread 1

void * blur_normal_wthread1(void *arg)
{
   using namespace blur_normal_names;

   int      index = *((int *) arg);
   int      rad = blur_radius;
   int      ii, dist = 0;
   int      Fend, px, py, qy;
   int      ylo, yhi;
   float    blend, R9, G9, B9, w1, w2;
   float    *pix2, *pix3;

   while (true)                                                                  //  loop all edit pixels
   {
      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) break;
      if (blend == 0) continue;
      
      if (Fescape) return 0;                                                     //  escape key 

      pix3 = PXMpix(E3pxm,px,py);                                                //  input pixel
      pix2 = PXMpix(E2pxm,px,py);                                                //  output pixel - intermediate image

      ylo = py - rad;                                                            //  get column of input pixel
      if (ylo < 0) ylo = 0;
      yhi = py + rad;
      if (yhi > Ehh-1) yhi = Ehh - 1;

      R9 = G9 = B9 = 0;
      w2 = 0;

      for (qy = ylo; qy <= yhi; qy++)                                            //  loop pixels in same column
      {
         if (sa_stat == sa_stat_fini) {
            ii = qy * Eww + px;                                                  //  don't use pixels outside area
            dist = sa_pixmap[ii];
            if (! dist) continue;
         }

         w1 = blur_weight[abs(qy-py)];                                           //  weight based on radius
         pix3 = PXMpix(E3pxm,px,qy);
         R9 += w1 * pix3[0];                                                     //  accumulate RGB * weight
         G9 += w1 * pix3[1];
         B9 += w1 * pix3[2];
         w2 += w1;                                                               //  accumulate weights
      }

      R9 = R9 / w2;                                                              //  normalize
      G9 = G9 / w2;
      B9 = B9 / w2;

      pix2[0] = R9;                                                              //  weighted average
      pix2[1] = G9;
      pix2[2] = B9;

      if (! Fpaintedits) 
         progress_addvalue(index,1);                                             //  track progress
   }

   return 0;
}


//  worker thread 2

void * blur_normal_wthread2(void *arg) 
{
   using namespace blur_normal_names;

   int      index = *((int *) arg);
   int      rad = blur_radius;
   int      ii, dist = 0;
   int      Fend, px, py, qx;
   int      xlo, xhi;
   float    R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float    blend, w1, w2;
   float    *pix1, *pix2, *pix3;

   while (true)                                                                  //  loop all edit pixels
   {
      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) break;
      if (blend == 0) continue;

      if (Fescape) return 0;                                                     //  user cancel

      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      R1 = pix1[0];                                                              //  input RGB values
      G1 = pix1[1];
      B1 = pix1[2];

      R3 = pix3[0];                                                              //  current output RGB
      G3 = pix3[1];
      B3 = pix3[2];
      
      xlo = px - rad;                                                            //  get row of input pixel
      if (xlo < 0) xlo = 0;
      xhi = px + rad;
      if (xhi > Eww-1) xhi = Eww - 1;

      R9 = G9 = B9 = 0;
      w2 = 0;

      for (qx = xlo; qx <= xhi; qx++)                                            //  loop pixels in same row
      {
         if (sa_stat == sa_stat_fini) {
            ii = py * Eww + qx;                                                  //  don't use pixels outside area
            dist = sa_pixmap[ii];
            if (! dist) continue;
         }

         pix2 = PXMpix(E2pxm,qx,py);                                             //  use intermediate image

         w1 = blur_weight[abs(qx-px)];                                           //  weight based on radius
         R9 += w1 * pix2[0];                                                     //  accumulate RGB * weight
         G9 += w1 * pix2[1];
         B9 += w1 * pix2[2];
         w2 += w1;                                                               //  accumulate weights
      }

      R9 = R9 / w2;                                                              //  normalize
      G9 = G9 / w2;
      B9 = B9 / w2;

      RGBfix(R9,G9,B9);

      if (Fpaintedits)                                                           //  gradual edit within mouse circle
      {
         if (blend > 0)
         {
            R3 = blend * R9 + (1-blend) * R3;                                    //  increase edit 
            G3 = blend * G9 + (1-blend) * G3;
            B3 = blend * B9 + (1-blend) * B3;
         }
         
         else if (blend < 0)
         {
            R3 = -blend * R1 + (1+blend) * R3;                                   //  decrease edit
            G3 = -blend * G1 + (1+blend) * G3;
            B3 = -blend * B1 + (1+blend) * B3;
         }
      }

      else                                                                       //  full edit for image or area
      {
         R3 = blend * R9 + (1-blend) * R1;                                       //  increase edit 
         G3 = blend * G9 + (1-blend) * G1;
         B3 = blend * B9 + (1-blend) * B1;
      }

      pix3[0] = R3;                                                              //  set output pixel
      pix3[1] = G3;
      pix3[2] = B3;

      if (! Fpaintedits)
         progress_addvalue(index,1);                                             //  track progress
   }

   return 0;
}


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

//  radial blur

namespace blur_radial_names
{
   editfunc    EFblurad;
   PXM         *E2pxm;
   int         RBrad, RBlen;                                                     //  radial blur radius, length
   int         Cx, Cy;                                                           //  image center of radial blur
}


void m_blur_radial(GtkWidget *, ch *menu)
{
   using namespace blur_radial_names;

   int    blur_radial_dialog_event(zdialog *zd, ch *event);
   void   blur_radial_mousefunc();
   void * blur_radial_thread(void *);

   F1_help_topic = "blur";

   Plog(1,"m_blur_radial \n");

   EFblurad.menuname = "Blur Radial";
   EFblurad.menufunc = m_blur_radial;
   EFblurad.Farea = 2;                                                           //  select area usable
   EFblurad.threadfunc = blur_radial_thread;                                     //  thread function
   EFblurad.mousefunc = blur_radial_mousefunc;

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

/***
          _________________________________________
         |            Radial Blur                  |
         |                                         |
         |  Radius [___] Length [___]              |                             //  central area, no blur
         |  Center X [___] Y [___]                 |
         |                                         |
         |                [Reset] [Apply] [OK] [X] |
         |_________________________________________|

***/

   zdialog *zd = zdialog_new("Radial Blur",Mwin,"Reset","Apply","OK"," X ",null);
   EFblurad.zd = zd;

   zdialog_add_widget(zd,"hbox","hbrb2","dialog");
   zdialog_add_widget(zd,"label","labrbr","hbrb2","Radius","space=5");
   zdialog_add_widget(zd,"zspin","RBrad","hbrb2","1|999|1|100","space=3|size=3");
   zdialog_add_widget(zd,"label","space","hbrb2",0,"space=5");
   zdialog_add_widget(zd,"label","labrbl","hbrb2","Length","space=5");
   zdialog_add_widget(zd,"zspin","RBlen","hbrb2","1|999|1|100","space=3|size=3");
   zdialog_add_widget(zd,"hbox","hbrb3","dialog");
   zdialog_add_widget(zd,"label","labc","hbrb3","Center","space=5");
   zdialog_add_widget(zd,"label","labcx","hbrb3","X","space=3");
   zdialog_add_widget(zd,"zentry","Cx","hbrb3",0,"space=3|size=3");
   zdialog_add_widget(zd,"label","space","hbrb3",0,"space=5");
   zdialog_add_widget(zd,"label","labcy","hbrb3","Y","space=3");
   zdialog_add_widget(zd,"zentry","Cy","hbrb3",0,"space=3|size=3");

   E2pxm = 0;

   RBrad = 100;
   RBlen = 100;
   Cx = Eww / 2;
   Cy = Ehh / 2;
   zdialog_stuff(zd,"Cx",Cx);
   zdialog_stuff(zd,"Cy",Cy);

   zdialog_load_inputs(zd);

   zdialog_run(zd,blur_radial_dialog_event,"save");                              //  run dialog

   return;
}


//  dialog event and completion callback function

int blur_radial_dialog_event(zdialog * zd, ch *event)
{
   using namespace blur_radial_names;

   void   blur_radial_mousefunc();

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

   zdialog_fetch(zd,"RBrad",RBrad);
   zdialog_fetch(zd,"RBlen",RBlen);
   zdialog_fetch(zd,"Cx",Cx);
   zdialog_fetch(zd,"Cy",Cy);

   takeMouse(blur_radial_mousefunc,dragcursor);

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  [reset]
      {
         zd->zstat = 0;                                                          //  keep dialog active
         edit_reset();
         return 1;
      }

      else if (zd->zstat == 2)                                                   //  [apply]
      {
         zd->zstat = 0;                                                          //  keep dialog active
         thread_signal();                                                        //  trigger thread
         return 1;                                                               //  do not free E2
      }

      else if (zd->zstat == 3) {                                                 //  [OK]
         edit_addhist("rad:%d length:%d",RBrad,RBlen);
         edit_done(0);
      }

      else edit_cancel(0);                                                       //  cancel

      if (E2pxm) PXM_free(E2pxm);                                                //  free memory
      E2pxm = 0;
   }

   return 1;
}


//  mouse function

void blur_radial_mousefunc()
{
   using namespace blur_radial_names;

   if (! CEF) return;

   if (LMclick)                                                                  //  radial blur, new center
   {
      zdialog *zd = CEF->zd;
      Cx = Mxposn;
      Cy = Myposn;
      zdialog_stuff(zd,"Cx",Cx);
      zdialog_stuff(zd,"Cy",Cy);
      LMclick = 0;
      thread_signal();
   }

   return;
}


//  thread function

void * blur_radial_thread(void *)
{
   using namespace blur_radial_names;

   void * blur_radial_wthread(void *);

   if (E2pxm) PXM_free(E2pxm);
   E2pxm = PXM_copy(E1pxm);                                                      //  use original image

   if (sa_stat == sa_stat_fini) progress_setgoal(sa_Npixel);                     //  initz. progress counter
   else  progress_setgoal(Eww * Ehh);

   do_wthreads(blur_radial_wthread,NSMP);                                        //  worker threads

   progress_setgoal(0);

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

   Fpaint2();
   return 0;
}


//  worker thread

void * blur_radial_wthread(void *arg)
{
   using namespace blur_radial_names;

   int      index = *((int *) arg);
   int      ii, dist = 0;
   int      px, py, qx, qy, qz;
   float    RBlen2;
   float    *pix2, *pix3;
   float    R, Rx, Ry, Rz;
   int      Rsum, Gsum, Bsum, Npix;

   for (py = index+1; py < Ehh-1; py += NSMP)                                    //  loop all image pixels
   for (px = 1; px < Eww-1; px++)
   {
      if (Fescape) return 0;                                                     //  user cancel

      ii = py * Eww + px;

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

      Rsum = Gsum = Bsum = Npix = 0;                                             //  reset RGB sums

      R = sqrtf((px-Cx)*(px-Cx) + (py-Cy)*(py-Cy));                              //  distance (Cx,Cy) to (px,py)
      if (R == 0) continue;
      Rx = (px-Cx)/R;                                                            //  unit vector along (Cx,Cy) to (px,py)
      Ry = (py-Cy)/R;
      if (fabsf(Rx) > fabsf(Ry)) Rz = 1.0 / fabsf(Rx);                           //  Rz is 1.0 .. 1.414
      else Rz = 1.0 / fabsf(Ry);
      Rx = Rx * Rz;                                                              //  vector with max x/y component = 1
      Ry = Ry * Rz;

      RBlen2 = Eww;                                                              //  lesser image dimension
      if (Ehh < Eww) RBlen2 = Ehh;
      RBlen2 = RBlen * (R - RBrad) / RBlen2;                                     //  R = 0-max >> RBlen2 = 0..RBlen
      if (RBlen2 < 1) RBlen2 = 1;                                                //  central area, no blur

      for (qz = 0; qz < RBlen2; qz++)                                            //  loop (qx,qy) from (px,py)
      {                                                                          //    in direction to (Cx,Cy)
         qx = px - qz * Rx;                                                      //      for distance RBlen
         qy = py - qz * Ry;
         if (qx < 0 || qx > Eww-1) break;
         if (qy < 0 || qy > Ehh-1) break;

         if (sa_stat == sa_stat_fini) {
            ii = qy * Eww + qx;                                                  //  don't use pixels outside area
            dist = sa_pixmap[ii];
            if (! dist) continue;
         }

         pix2 = PXMpix(E2pxm,qx,qy);                                             //  sum RGB values
         Rsum += pix2[0];
         Gsum += pix2[1];
         Bsum += pix2[2];
         Npix++;                                                                 //  count pixels in sum
      }

      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel is average of
      pix3[0] = Rsum / Npix;                                                     //   pixels in line of length RBlen
      pix3[1] = Gsum / Npix;                                                     //    from (px,py) to (Cx,Cy)
      pix3[2] = Bsum / Npix;

      progress_addvalue(index,1);                                                //  track progress
   }

   return 0;
}


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

//  directed blur

namespace blur_directed_names
{
   float       Dmdx, Dmdy, Dmdw, Dmdh;
   float       DD, Dspan, Dintens;
   editfunc    EFblurdirc;
}

void m_blur_directed(GtkWidget *, ch *menu)
{
   using namespace blur_directed_names;

   int    blur_directed_dialog_event(zdialog *zd, ch *event);
   void   blur_directed_mousefunc();
   void * blur_directed_thread(void *);

   F1_help_topic = "blur";

   Plog(1,"m_blur_directed \n");

   EFblurdirc.menuname = "Blur Directed";
   EFblurdirc.menufunc = m_blur_directed;
   EFblurdirc.Farea = 2;                                                         //  select area usable
   EFblurdirc.threadfunc = blur_directed_thread;                                 //  thread function
   EFblurdirc.mousefunc = blur_directed_mousefunc;

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

/***
          _________________________________________
         |             Directed Blur               |
         |                                         |
         |  Blur Span  [___]  Intensity   [___]    |
         |                                         |
         |                        [Reset] [OK] [X] |
         |_________________________________________|

***/

   zdialog *zd = zdialog_new("Directed Blur",Mwin,"Reset","OK"," X ",null);
   EFblurdirc.zd = zd;

   zdialog_add_widget(zd,"hbox","hbdb2","dialog");
   zdialog_add_widget(zd,"label","labspan","hbdb2","Blur Span","space=5");
   zdialog_add_widget(zd,"zspin","span","hbdb2","0.00|1.0|0.002|0.1","space=3|size=3");
   zdialog_add_widget(zd,"label","space","hbdb2",0,"space=5");
   zdialog_add_widget(zd,"label","labint","hbdb2","Intensity");
   zdialog_add_widget(zd,"zspin","intens","hbdb2","0.00|1.0|0.01|0.2","space=3|size=3");

   Dspan = 0.1;
   Dintens = 0.2;

   zdialog_load_inputs(zd);

   zdialog_run(zd,blur_directed_dialog_event,"save");                            //  run dialog

   return;
}


//  dialog event and completion callback function

int blur_directed_dialog_event(zdialog * zd, ch *event)
{
   using namespace blur_directed_names;

   void   blur_directed_mousefunc();

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

   takeMouse(blur_directed_mousefunc,dragcursor);

   zdialog_fetch(zd,"span",Dspan);
   zdialog_fetch(zd,"intens",Dintens);

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  [reset]
      {
         zd->zstat = 0;                                                          //  keep dialog active
         edit_reset();
      }

      else if (zd->zstat == 2) {                                                 //  [OK]
         edit_addhist("span:%.2f intens:%.2f",Dspan,Dintens);
         edit_done(0);
      }

      else edit_cancel(0);                                                       //  discard edit
   }

   return 1;
}


//  mouse function

void blur_directed_mousefunc()
{
   using namespace blur_directed_names;

   if (! CEF) return;

   if (Mxdrag || Mydrag)                                                         //  directed blur, mouse drag
   {
      Dmdx = Mxdown;                                                             //  drag origin
      Dmdy = Mydown;
      Dmdw = Mxdrag - Mxdown;                                                    //  drag increment
      Dmdh = Mydrag - Mydown;
      Mxdrag = Mydrag = 0;
      thread_signal();
   }

   return;
}


//  thread function

void * blur_directed_thread(void *)
{
   using namespace blur_directed_names;

   void * blur_directed_wthread(void *);

   float       dd, d1, d2, d3, d4;

   d1 = (Dmdx) * (Dmdx) + (Dmdy) * (Dmdy);                                       //  distance, mouse to 4 corners
   d2 = (Eww-Dmdx) * (Eww-Dmdx) + (Dmdy) * (Dmdy);
   d3 = (Eww-Dmdx) * (Eww-Dmdx) + (Ehh-Dmdy) * (Ehh-Dmdy);
   d4 = (Dmdx) * (Dmdx) + (Ehh-Dmdy) * (Ehh-Dmdy);

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

   DD = dd * 0.5 * Dspan;

   do_wthreads(blur_directed_wthread,NSMP);                                      //  worker threads

   progress_setgoal(0);

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

   Fpaint2();
   return 0;
}


//  worker thread

void * blur_directed_wthread(void *arg)
{
   using namespace blur_directed_names;

   int      index = *((int *) arg);
   int      ii, px, py, dist = 0, vstat;
   float    d, mag, dispx, dispy;
   float    F1, F2;
   float    vpix[4], *pix3;

   F1 = Dintens * Dintens;
   F2 = 1.0 - F1;

   for (py = index; py < Ehh; py += NSMP)                                        //  process all pixels
   for (px = 0; px < Eww; px++)
   {
      ii = py * Eww + px;

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

      d = (px-Dmdx)*(px-Dmdx) + (py-Dmdy)*(py-Dmdy);
      mag = (1.0 - d / DD);
      if (mag < 0) continue;

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

      dispx = -Dmdw * mag;                                                       //  displacement = drag * mag
      dispy = -Dmdh * mag;

      vstat = vpixel(E3pxm,px+dispx,py+dispy,vpix);                              //  input virtual pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      if (vstat) {
         pix3[0] = F2 * pix3[0] + F1 * vpix[0];                                  //  output = input pixel blend
         pix3[1] = F2 * pix3[1] + F1 * vpix[1];
         pix3[2] = F2 * pix3[2] + F1 * vpix[2];
      }
   }

   return 0;                                                                     //  exit thread
}


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

//  graduated blur

namespace blur_graduated_names
{
   float       radius;                                                           //  blur radius
   int         con_limit;                                                        //  contrast limit
   uint8       *pixcon;
   int         pixseq_done[122][122];                                            //  up to blur_radius = 60
   int         pixseq_angle[1000];
   int         pixseq_dx[13000], pixseq_dy[13000];
   int         pixseq_rad[13000];
   int         max1 = 999, max2 = 12999;                                         //  for later overflow check
   editfunc    EFblurgrad;
}


void m_blur_graduated(GtkWidget *, ch *menu)
{
   using namespace blur_graduated_names;

   void   blur_graduated_pixcon(void);
   void   blur_graduated_pixseq(void);
   int    blur_graduated_dialog_event(zdialog *zd, ch *event);
   void * blur_graduated_thread(void *);

   F1_help_topic = "blur";

   Plog(1,"m_blur_graduated \n");

   EFblurgrad.menuname = "Blur Graduated";
   EFblurgrad.menufunc = m_blur_graduated;
   EFblurgrad.Farea = 2;                                                         //  select area usable
   EFblurgrad.threadfunc = blur_graduated_thread;                                //  thread function
   EFblurgrad.Fpaintedits = 1;                                                   //  allow paint edits

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

/***
          _________________________________________
         |           Graduated Blur                |
         |                                         |
         |  Radius [___]  Contrast Limit [___]     |
         |                                         |
         |                [Reset] [Apply] [OK] [X] |
         |_________________________________________|

***/

   zdialog *zd = zdialog_new("Graduated Blur",Mwin,"Reset","Apply","OK"," X ",null);
   EFblurgrad.zd = zd;

   zdialog_add_widget(zd,"hbox","hbgb2","dialog");
   zdialog_add_widget(zd,"label","labgrad","hbgb2","Radius","space=5");
   zdialog_add_widget(zd,"zspin","radius","hbgb2","1|50|1|10","space=3|size=3");
   zdialog_add_widget(zd,"label","space","hbgb2",0,"space=5");
   zdialog_add_widget(zd,"label","lablim","hbgb2","Contrast Limit");
   zdialog_add_widget(zd,"zspin","con_limit","hbgb2","1|255|1|50","space=3|size=3");

   radius = 10;                                                                  //  initial values
   con_limit = 1;

   zdialog_run(zd,blur_graduated_dialog_event,"save");                           //  run dialog
   zmainsleep(0.1);

   blur_graduated_pixcon();                                                      //  initialize - will burn a while
   blur_graduated_pixseq();

   return;
}


//  dialog event and completion callback function

int blur_graduated_dialog_event(zdialog * zd, ch *event)
{
   using namespace blur_graduated_names;

   void   blur_graduated_pixseq(void);

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

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  [reset]
      {
         zd->zstat = 0;                                                          //  keep dialog active
         edit_reset();
         return 1;
      }

      if (zd->zstat == 2)                                                        //  [apply]
      {
         zd->zstat = 0;                                                          //  keep dialog active
         thread_signal();                                                        //  trigger thread
         return 1;                                                               //  do not free E2
      }

      if (zd->zstat == 3) {                                                      //  [OK]
         edit_addhist("rad:%.0f conlim:%d",radius,con_limit);
         edit_done(0);
      }

      else edit_cancel(0);                                                       //  cancel

      zfree(pixcon);
      return 1;
   }

   if (strmatch(event,"radius")) {
      zdialog_fetch(zd,"radius",radius);
      blur_graduated_pixseq();
   }
      
   zdialog_fetch(zd,"con_limit",con_limit);
   
   if (strmatch(event,"paint"))                                                  //  from paint edits
      thread_signal();

   return 1;
}


//  calculate contrast for all pixels - done once

void blur_graduated_pixcon(void)
{
   using namespace blur_graduated_names;

   int         ii;
   int         px, py, dx, dy;
   float       *pix1, *pix2;
   float       contrast, maxcon;

   Funcbusy(+1);

   pixcon = (uint8 *) zmalloc(Eww * Ehh,"grad blur");                            //  pixel contrast map

   for (py = 1; py < Ehh-1; py++)                                                //  loop interior pixels
   for (px = 1; px < Eww-1; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);                                                //  this pixel in base image E1
      contrast = maxcon = 0.0;

      for (dx = px-1; dx <= px+1; dx++)                                          //  loop neighbor pixels
      for (dy = py-1; dy <= py+1; dy++)
      {
         pix2 = PXMpix(E1pxm,dx,dy);
         contrast = 1.0 - PIXmatch(pix1,pix2);                                   //  contrast, 0-1
         if (contrast > maxcon) maxcon = contrast;
      }

      ii = py * Eww + px;                                                        //  ii maps to (px,py)
      pixcon[ii] = 255 * maxcon;                                                 //  pixel contrast, 0 to 255
   }

   Funcbusy(-1);
   return;
}


//  map pixels around each image pixel in order of increasing angle and radius
//  done whenever radius changes

void blur_graduated_pixseq(void)
{
   using namespace blur_graduated_names;

   int         ii, jj;
   int         dx, dy, adx, ady;
   float       rad1, rad2, angle, astep;

   Funcbusy(+1);

   rad1 = radius;

   for (dy = 0; dy <= 2*rad1; dy++)                                              //  no pixels mapped yet
   for (dx = 0; dx <= 2*rad1; dx++)
      pixseq_done[dx][dy] = 0;

   ii = jj = 0;

   astep = 0.5 / rad1;                                                           //  0.5 pixel steps at rad1 from center

   for (angle = 0; angle < 2*PI; angle += astep)                                 //  loop full circle
   {
      pixseq_angle[ii] = jj;                                                     //  start pixel sequence for this angle
      ii++;

      for (rad2 = 1; rad2 <= rad1; rad2++)                                       //  loop rad2 from center to edge
      {
         dx = lround(rad2 * cos(angle));                                         //  pixel at angle and rad2
         dy = lround(rad2 * sin(angle));
         adx = rad1 + dx;
         ady = rad1 + dy;
         if (pixseq_done[adx][ady]) continue;                                    //  pixel already mapped
         pixseq_done[adx][ady] = 1;                                              //  map pixel
         pixseq_dx[jj] = dx;                                                     //  save pixel sequence for angle
         pixseq_dy[jj] = dy;
         pixseq_rad[jj] = rad2;                                                  //  pixel radius
         jj++;
      }
      pixseq_rad[jj] = 9999;                                                     //  mark end of pixels for angle
      jj++;
   }

   pixseq_angle[ii] = 9999;                                                      //  mark end of angle steps

   if (ii > max1 || jj > max2)                                                   //  should not happen
      zappcrash("gradblur array overflow");
   
   Funcbusy(-1);
   return;
}


//  thread function

void * blur_graduated_thread(void *)
{
   using namespace blur_graduated_names;

   void * blur_graduated_wthread(void *);
   
   if (Fpaintedits && ! Mdrag) return 0;                                         //  if mouse paint, must be drag 
   
   if (! Fpaintedits)
   {
      if (sa_stat == sa_stat_fini) progress_setgoal(sa_Npixel);                  //  initz. progress counter
      else  progress_setgoal(Eww * Ehh);
   }

   get_edit_pixels_init(NSMP,0);
   do_wthreads(blur_graduated_wthread,NSMP);                                     //  worker threads

   progress_setgoal(0);

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

   if (! Fpaintedits) Fpaint2();

   return 0;
}


//  worker thread

void * blur_graduated_wthread(void *arg)
{
   using namespace blur_graduated_names;

   int      index = *((int *) arg);
   int      ii, jj, npix, Fend;
   int      px, py, dx, dy;
   float    blend;
   float    *pix1, *pix3, *pixN;
   float    R1, G1, B1, R3, G3, B3, R9, G9, B9;
   int      rad, blurrad, con;

   while (true)                                                                  //  loop all edit pixels
   {
      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) break;
      if (blend == 0) continue;

      if (Fescape) return 0;                                                     //  user kill

      ii = py * Eww + px;

      if (pixcon[ii] > con_limit) {
         progress_addvalue(index,1);                                             //  high contrast pixel
         continue;
      }

      pix1 = PXMpix(E1pxm,px,py);                                                //  source pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  target pixel

      R1 = pix1[0];                                                              //  blur center pixel
      G1 = pix1[1];
      B1 = pix1[2];

      R3 = R9 = pix3[0];                                                         //  current output RGB
      G3 = G9 = pix3[1];
      B3 = B9 = pix3[2];

      R9 = R3;
      G9 = G3;
      B9 = B3;

      npix = 1;

      blurrad = 1.0 + radius * (con_limit - pixcon[ii]) / con_limit;             //  blur radius for pixel, 1 - blur_radius

      for (ii = 0; ii < 2000; ii++)                                              //  loop angle around center pixel
      {
         jj = pixseq_angle[ii];                                                  //  1st pixel for angle step ii
         if (jj == 9999) break;                                                  //  none, end of angle loop

         while (true)                                                            //  loop pixels from center to radius
         {
            rad = pixseq_rad[jj];                                                //  next pixel step radius
            if (rad > blurrad) break;                                            //  stop here if beyond blur radius
            dx = pixseq_dx[jj];                                                  //  next pixel step px/py
            dy = pixseq_dy[jj];
            if (px+dx < 0 || px+dx > Eww-1) break;                               //  stop here if off the edge
            if (py+dy < 0 || py+dy > Ehh-1) break;

            pixN = PXMpix(E1pxm,px+dx,py+dy);
            con = 255 * (1.0 - PIXmatch(pix1,pixN));
            if (con > con_limit) break;

            R9 += pixN[0];                                                       //  add pixel RGB values
            G9 += pixN[1];
            B9 += pixN[2];
            npix++;
            jj++;
         }
      }

      R9 = R9 / npix;                                                            //  average pixel values
      G9 = G9 / npix;
      B9 = B9 / npix;

      RGBfix(R9,G9,B9);

      if (Fpaintedits)                                                           //  gradual edit within mouse circle
      {
         if (blend > 0)
         {
            R3 = blend * R9 + (1-blend) * R3;                                    //  increase edit 
            G3 = blend * G9 + (1-blend) * G3;
            B3 = blend * B9 + (1-blend) * B3;
         }
         
         else if (blend < 0)
         {
            R3 = -blend * R1 + (1+blend) * R3;                                   //  decrease edit
            G3 = -blend * G1 + (1+blend) * G3;
            B3 = -blend * B1 + (1+blend) * B3;
         }
      }

      else                                                                       //  full edit for image or area
      {
         R3 = blend * R9 + (1-blend) * R1;                                       //  increase edit 
         G3 = blend * G9 + (1-blend) * G1;
         B3 = blend * B9 + (1-blend) * B1;
      }

      pix3[0] = R3;                                                              //  set output pixel
      pix3[1] = G3;
      pix3[2] = B3;

      if (! Fpaintedits)
         progress_addvalue(index,1);                                             //  track progress
   }

   return 0;
}


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

//  Blur Background
//  Blur the image outside of a selected area or areas.
//  Blur increases with distance from selected area edges.

namespace blur_background_names
{
   int         conrad, incrad;               //  constant or increasing blur
   int         conbrad;                      //  constant blur radius
   int         minbrad;                      //  min. blur radius
   int         maxbrad;                      //  max. blur radius
   int         maxdist;                      //  max. area edge distance
   editfunc    EFblurbg;
}


void m_blur_background(GtkWidget *, ch *menu)
{
   using namespace blur_background_names;

   int    blur_background_dialog_event(zdialog* zd, ch *event);
   void * blur_background_thread(void *);

   Plog(1,"m_blur_background \n");

   EFblurbg.menufunc = m_blur_background;
   EFblurbg.menuname = "Blur Background";                                        //  function name
   EFblurbg.Farea = 2;                                                           //  select area usable (required)
   EFblurbg.threadfunc = blur_background_thread;                                 //  thread function

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

   minbrad = 10;                                                                 //  defaults
   maxbrad = 20;
   conbrad = 10;
   conrad = 1;
   incrad = 0;

/***
       ____________________________________
      |          Blur Background           |
      |                                    |
      |  [x] constant blur [ 20 ]          |
      |                                    |
      |  [x] increase blur with distance   |
      |    min. blur radius [ 20 ]         |
      |    max. blur radius [ 90 ]         |
      |                                    |
      |                   [Apply] [OK] [X] |
      |____________________________________|

***/

   zdialog *zd = zdialog_new("Blur Background",Mwin,"Apply","OK"," X ",null);
   CEF->zd = zd;

   zdialog_add_widget(zd,"hbox","hbcon","dialog",0,"space=5");
   zdialog_add_widget(zd,"check","conrad","hbcon","constant blur","space=3");
   zdialog_add_widget(zd,"zspin","conbrad","hbcon","1|100|1|10","space=8");
   zdialog_add_widget(zd,"hbox","hbinc","dialog");
   zdialog_add_widget(zd,"check","incrad","hbinc","increase blur with distance","space=3");
   zdialog_add_widget(zd,"hbox","hbmin","dialog");
   zdialog_add_widget(zd,"label","labmin","hbmin","min. blur radius","space=8");
   zdialog_add_widget(zd,"zspin","minbrad","hbmin","0|100|1|10","space=3");
   zdialog_add_widget(zd,"hbox","hbmax","dialog");
   zdialog_add_widget(zd,"label","labmax","hbmax","max. blur radius","space=8");
   zdialog_add_widget(zd,"zspin","maxbrad","hbmax","1|100|1|20","space=3");

   zdialog_stuff(zd,"conrad",conrad);
   zdialog_stuff(zd,"incrad",incrad);

   zdialog_resize(zd,300,0);
   zdialog_load_inputs(zd);                                                      //  restore previous inputs
   zdialog_run(zd,blur_background_dialog_event,"save");                          //  run dialog - parallel

   if (sa_stat != sa_stat_fini) m_select_area(0,0);                              //  start select area dialog
   return;
}


//  dialog event and completion function

int blur_background_dialog_event(zdialog *zd, ch *event)                         //  blur_background dialog event function
{
   using namespace blur_background_names;

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

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  [apply]
      {
         zd->zstat = 0;                                                          //  keep dialog active

         if (sa_stat != sa_stat_fini) {
            zmessageACK(Mwin,"no active Select Area");
            m_select_area(0,0);
            return 1;
         }

         if (incrad && ! sa_edgecalc_done)                                       //  if increasing blur radius,
            sa_edgecalc();                                                       //    calc. area edge distances

         sa_show(0,0);
         edit_reset();
         thread_signal();
         return 1;
      }

      else if (zd->zstat == 2) {                                                 //  [OK]
         if (zd_sela) zdialog_send_event(zd_sela,"done");                        //  kill select area dialog
         if (conrad) edit_addhist("rad:%d",conrad);
         if (incrad) edit_addhist("rad:%d-%d",minbrad,maxbrad);
         edit_done(0);
      }

      else {                                                                     //  discard edit
         edit_cancel(0);
         if (zd_sela) zdialog_send_event(zd_sela,"done");                        //  kill select area dialog
      }

      return 1;
   }

   if (zstrstr("conrad incrad",event)) {
      zdialog_stuff(zd,"conrad",0);
      zdialog_stuff(zd,"incrad",0);
      zdialog_stuff(zd,event,1);
   }

   zdialog_fetch(zd,"conrad",conrad);
   zdialog_fetch(zd,"incrad",incrad);
   zdialog_fetch(zd,"conbrad",conbrad);
   zdialog_fetch(zd,"minbrad",minbrad);
   zdialog_fetch(zd,"maxbrad",maxbrad);

   return 1;
}


//  thread function

void * blur_background_thread(void *)
{
   using namespace blur_background_names;

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

   int      ii, dist;

   if (incrad && sa_edgecalc_done) {                                             //  if increasing blur radius,
      maxdist = 0;                                                               //    get max. area edge distance
      for (ii = 0; ii < Eww * Ehh; ii++) {
         dist = sa_pixmap[ii];
         if (dist > maxdist) maxdist = dist;
      }
   }

   progress_setgoal(sa_Npixel);                                                  //  initz. progress counter
   Fescape = 0;

   do_wthreads(blur_background_wthread,NSMP);                                    //  worker threads

   progress_setgoal(0);

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

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


//  worker thread function

void * blur_background_wthread(void *arg)
{
   using namespace blur_background_names;

   int         index = *((int *) (arg));
   int         ii, rad = 0, dist, npix;
   int         px, py, qx, qy;
   float       *pix1, *pix3;
   float       red, green, blue, F;

   for (py = index; py < Ehh; py += NSMP)                                        //  loop all image pixels
   for (px = 0; px < Eww; px++)
   {
      if (Fescape) break;                                                        //  cancel edit

      ii = py * Eww + px;
      dist = sa_pixmap[ii];                                                      //  area edge distance
      if (! dist) continue;                                                      //  pixel outside the area

      if (conrad) rad = conbrad;                                                 //  use constant blur radius

      if (incrad) {                                                              //  use increasing blur radius
         if (! sa_edgecalc_done) return 0;                                       //    depending on edge distance
         rad = minbrad + (maxbrad - minbrad) * dist / maxdist;
      }

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

      for (qy = py-rad; qy <= py+rad; qy++)                                      //  average surrounding pixels
      for (qx = px-rad; qx <= px+rad; qx++)
      {
         if (qy < 0 || qy > Ehh-1) continue;
         if (qx < 0 || qx > Eww-1) continue;
         ii = qy * Eww + qx;
         if (! sa_pixmap[ii]) continue;
         pix1 = PXMpix(E1pxm,qx,qy);
         red += pix1[0];
         green += pix1[1];
         blue += pix1[2];
         npix++;
      }

      F = 0.999 / npix;
      red = F * red;                                                             //  blurred pixel RGB
      green = F * green;
      blue = F * blue;
      pix3 = PXMpix(E3pxm,px,py);
      pix3[0] = red;
      pix3[1] = green;
      pix3[2] = blue;

      progress_addvalue(index,1);                                                //  count pixels done
   }

   return 0;                                                                     //  exit thread
}


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

//  add motion blur to selected areas in an image

namespace  blur_motion_names
{
   editfunc    EFblurmot;
   int         span;                                                             //  blur span, pixels
   int         angle;                                                            //  blur angle, 0-180 deg.
   PXM         *tempxm;
}


void m_blur_motion(GtkWidget *, ch *menu)
{
   using namespace blur_motion_names;

   void   blur_motion_mousefunc();
   int    blur_motion_dialog_event(zdialog* zd, ch *event);
   void * blur_motion_thread(void *);

   ch    *hintmess = "Drag mouse across image \n"
                     " to indicate blur direction";

   Plog(1,"m_blur_motion \n");

   EFblurmot.menufunc = m_blur_motion;
   EFblurmot.menuname = "Blur Motion";
   EFblurmot.Farea = 2;                                                          //  select area usable
   EFblurmot.threadfunc = blur_motion_thread;                                    //  thread function
   EFblurmot.mousefunc = blur_motion_mousefunc;                                  //  mouse function

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

/***
          ___________________________________
         |         Add Motion Blur           |
         |                                   |
         | Drag mouse across image           |
         |  to indicate blur direction       |
         |                                   |
         | Blur Span (pixels) [___]          |
         | Blur Angle (degrees) [___]        |
         |                                   |
         |          [Reset] [Apply] [OK] [X] |
         |___________________________________|

***/

   zdialog *zd = zdialog_new("Add Motion Blur",Mwin,"Reset","Apply","OK"," X ",null);
   CEF->zd = zd;

   zdialog_add_widget(zd,"label","labhint","dialog",hintmess,"space=5");
   zdialog_add_widget(zd,"hbox","hbspan","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labspan","hbspan","Blur Span (pixels)","space=5");
   zdialog_add_widget(zd,"zspin","span","hbspan","0|50|1|0");
   zdialog_add_widget(zd,"label","space","hbspan",0,"space=20");
   zdialog_add_widget(zd,"hbox","hbangle","dialog");
   zdialog_add_widget(zd,"label","labangle","hbangle","Blur Angle (degrees)","space=5");
   zdialog_add_widget(zd,"zspin","angle","hbangle","0|180|1|0");
   zdialog_add_widget(zd,"label","space","hbangle",0,"space=20");

   angle = 0;
   span = 0;

   takeMouse(blur_motion_mousefunc,dragcursor);                                  //  connect mouse

   zdialog_run(zd,blur_motion_dialog_event);                                     //  run dialog - parallel

   if (sa_stat != sa_stat_fini) m_select_area(0,0);                              //  start select area dialog

   return;
}


//  dialog event and completion function

int blur_motion_dialog_event(zdialog *zd, ch *event)
{
   using namespace blur_motion_names;

   void  blur_motion_mousefunc();
   void  blur_motion_update();

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

   if (strmatch(event,"focus"))
      takeMouse(blur_motion_mousefunc,dragcursor);                               //  connect mouse

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  [reset]
         zd->zstat = 0;                                                          //  keep dialog active
         edit_reset();
      }

      else if (zd->zstat == 2) {                                                 //  [apply]
         zd->zstat = 0;                                                          //  keep dialog active
         blur_motion_update();
      }

      else if (zd->zstat == 3) {                                                 //  [OK] commit edit
         edit_addhist("span:%d angle:%d",span,angle);
         edit_done(0);
      }

      else edit_cancel(0);                                                       //  discard edit

      Fpaint2();
      return 1;
   }

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

   if (strmatch(event,"angle")) {                                                //  angle input
      zdialog_fetch(zd,"angle",angle);
      if (angle < 0) {
         angle = 180 + angle;                                                    //  -1 --> 179
         zdialog_stuff(zd,"angle",angle);
      }
      if (angle >= 180) {                                                        //  180 --> 0
         angle = angle - 180;
         zdialog_stuff(zd,"angle",angle);
      }
   }

   return 1;
}


//  mouse function - capture mouse drag direction to set image rotate angle

void blur_motion_mousefunc()
{
   using namespace blur_motion_names;

   int         dx, dy;
   float       R;

   if (! Mxdrag && ! Mydrag) return;

   dx = Mxdrag - Mxdown;                                                         //  drag vector
   dy = Mydrag - Mydown;
   Mxdrag = Mydrag = 0;

   R = sqrtf(dx*dx + dy*dy);                                                     //  get angle of drag
   angle = RAD * acosf(dx/R);
   if (dy > 0) angle = 180 - angle;                                              //  top quadrant only, 0-180 deg.
   if (angle == 180) angle = 0;
   if (CEF) zdialog_stuff(CEF->zd,"angle",angle);

   return;
}


//  add motion blur with specified span and angle

void blur_motion_update()
{
   using namespace blur_motion_names;

   int      rotate, mww, mhh, ii, dist;
   int      px, py;
   float    *pix1, *pix3;
   int      pcc = E1pxm->nc * sizeof(float);

   if (angle <= 90) rotate = angle;                                              //  avoid upside-down result
   else rotate = angle - 180;

   E9pxm = PXM_rotate(E1pxm,rotate);                                             //  rotate image so blur is horizontal
   thread_signal();                                                              //  add blur to E9
   thread_wait();
   tempxm = PXM_rotate(E9pxm,-rotate);                                           //  unrotate E9
   PXM_free(E9pxm);
   E9pxm = tempxm;

   mww = (E9pxm->ww - Eww) / 2;                                                  //  margins added by 2 rotates
   mhh = (E9pxm->hh - Ehh) / 2;

   PXM_copy_area(E9pxm,E3pxm,mww,mhh,0,0,Eww,Ehh);                               //  copy E9 (-margins) into E3
   PXM_free(E9pxm);
   E9pxm = 0;

   if (sa_stat == sa_stat_fini)                                                  //  select area present
   {
      for (py = 0; py < Ehh; py++)                                               //  replace output with input image
      for (px = 0; px < Eww; px++)                                               //    in areas outside the select area
      {
         ii = py * Eww + px;
         dist = sa_pixmap[ii];
         if (dist) continue;
         pix1 = PXMpix(E1pxm,px,py);
         pix3 = PXMpix(E3pxm,px,py);
         memcpy(pix3,pix1,pcc);
      }
   }

   return;
}


//  thread function - multiple working threads to update image

void * blur_motion_thread(void *)
{
   using namespace blur_motion_names;

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

   tempxm = PXM_copy(E9pxm);
   progress_setgoal(tempxm->hh);                                                 //  progress monitor

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

   progress_setgoal(0);
   PXM_free(tempxm);

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

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


//  add motion blur worker thread function
//  span = N: each blurred pixel is average of N original pixels

void * blur_motion_wthread(void *arg)
{
   using namespace blur_motion_names;

   int      index = *((int *) arg);
   int      px, py, ii;
   int      span2 = (span+1) / 2, span3 = 2 * span2 + 1;                         //  use even number
   float    *pix1, *pix9;
   float    R, G, B;
   int      Tww = tempxm->ww;                                                    //  input image dimensions
   int      Thh = tempxm->hh;

   for (py = index; py < Thh; py += NSMP)                                        //  loop all image pixels
   {
      progress_addvalue(index,1);                                                //  count progress

      for (px = span2+1; px < Tww-span2; px++)
      {
         R = G = B = 0;

         for (ii = -span2; ii <= span2; ii++)                                    //  sum input pixels
         {
            pix1 = PXMpix(tempxm,px-ii,py);
            R += pix1[0];
            G += pix1[1];
            B += pix1[2];
         }

         R = R / span3;                                                          //  average input pixels
         G = G / span3;
         B = B / span3;

         pix9 = PXMpix(E9pxm,px,py);                                             //  output pixel (blurred)
         pix9[0] = R;
         pix9[1] = G;
         pix9[2] = B;
      }
   }

   return 0;                                                                     //  exit thread
}


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

//  remove blur from camera motion - Richardson-Lucy method

namespace  fix_motionblur_names
{
   editfunc    EFfixmotionblur;
   int         span;                                                             //  blur span, pixels
   int         angle;                                                            //  blur angle, 0-180 deg.
   int         iter;                                                             //  algorithm iterations
   int         supring;                                                          //  suppress ringing
   PXM         *E2pxm;
}


//  menu function

void m_fix_motionblur(GtkWidget *, ch *menu)
{
   using namespace fix_motionblur_names;

   void   fix_motionblur_mousefunc();
   int    fix_motionblur_dialog_event(zdialog* zd, ch *event);
   void * fix_motionblur_thread(void *);

   ch    *hintmess = "Shift + drag mouse across image \n"
                     " to indicate blur direction";

   Plog(1,"m_fix_motionblur \n");

   EFfixmotionblur.menufunc = m_fix_motionblur;
   EFfixmotionblur.menuname = "Fix Motion Blur";
   EFfixmotionblur.Farea = 2;                                                    //  select area usable
   EFfixmotionblur.threadfunc = fix_motionblur_thread;                           //  thread function
   EFfixmotionblur.mousefunc = fix_motionblur_mousefunc;                         //  mouse function

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

/***
          ___________________________________
         |         Fix Motion Blur           |
         |                                   |
         | Shift + drag mouse across image   |
         |  to indicate blur direction       |
         |                                   |
         |  Blur Span (pixels)   [___]       |
         | Blur Angle (degrees)  [___]       |
         | Algorithm Iterations  [___]       |
         |   Suppress Ringing    [___]       |
         |                                   |
         |          [Reset] [Apply] [OK] [X] |
         |___________________________________|

***/

   zdialog *zd = zdialog_new("Fix Motion Blur",Mwin,"Reset","Apply","OK"," X ",null);
   CEF->zd = zd;

   zdialog_add_widget(zd,"label","labhint","dialog",hintmess,"space=5");

   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"label","space","hb1",0,"space=3");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=4|homog");
   zdialog_add_widget(zd,"label","space","hb1",0,"space=8");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"space=4|homog");
   zdialog_add_widget(zd,"label","space","hb1",0,"space=3");
   zdialog_add_widget(zd,"label","labspan","vb1","Blur Span (pixels)");
   zdialog_add_widget(zd,"label","labangle","vb1","Blur Angle (degrees)");
   zdialog_add_widget(zd,"label","labiter","vb1","Algorithm Iterations");
   zdialog_add_widget(zd,"label","labsup","vb1","Suppress Ringing");
   zdialog_add_widget(zd,"zspin","span","vb2","0|40|1|0");
   zdialog_add_widget(zd,"zspin","angle","vb2","-180|180|1|0");
   zdialog_add_widget(zd,"zspin","iter","vb2","0|100|1|0");
   zdialog_add_widget(zd,"zspin","supring","vb2","0|9|1|0");

   zdialog_load_inputs(zd);
   zdialog_run(zd,fix_motionblur_dialog_event);                                  //  run dialog - parallel
   return;
}


//  dialog event and completion function

int fix_motionblur_dialog_event(zdialog *zd, ch *event)
{
   using namespace fix_motionblur_names;

   void  fix_motionblur_mousefunc();

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  [reset]
         zd->zstat = 0;                                                          //  keep dialog active
         edit_reset();
      }

      else if (zd->zstat == 2) {                                                 //  [apply]
         zd->zstat = 0;                                                          //  keep dialog active
         edit_reset();
         zdialog_fetch(zd,"angle",angle);                                        //  get all inputs
         zdialog_fetch(zd,"span",span);
         zdialog_fetch(zd,"iter",iter);
         zdialog_fetch(zd,"supring",supring);

         if (angle < 0) {
            angle = 180 + angle;                                                 //  -1 --> 179
            zdialog_stuff(zd,"angle",angle);
         }
         if (angle >= 180) {                                                     //  180 --> 0
            angle = angle - 180;
            zdialog_stuff(zd,"angle",angle);
         }

         thread_signal();                                                        //  process
      }

      else if (zd->zstat == 3)                                                   //  [OK] commit edit
      {
         edit_addhist("span:%d angle:%d iter:%d supR:%d",                        //  edit params > edit hist
                                       span,angle,iter,supring);
         edit_done(0);
      }

      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   if (strmatch(event,"focus")) {
      takeMouse(fix_motionblur_mousefunc,0);                                     //  connect mouse
      return 1;
   }

   return 1;
}


//  mouse function - capture mouse drag direction and set rotate angle

void fix_motionblur_mousefunc()
{
   using namespace fix_motionblur_names;

   int         dx, dy;
   float       R;

   if (! KBshiftkey) {
      takeMouse(fix_motionblur_mousefunc,0);
      return;
   }

   takeMouse(fix_motionblur_mousefunc,dragcursor);

   if (! Mxdrag && ! Mydrag) return;

   dx = Mxdrag - Mxdown;                                                         //  drag vector
   dy = Mydrag - Mydown;
   Mxdrag = Mydrag = 0;

   R = sqrtf(dx*dx + dy*dy);                                                     //  get angle of drag
   angle = RAD * acosf(dx/R);
   if (dy > 0) angle = 180 - angle;                                              //  top quadrant only, 0-180 deg.
   if (angle == 180) angle = 0;
   if (CEF) zdialog_stuff(CEF->zd,"angle",angle);

   return;
}


//  thread function

void * fix_motionblur_thread(void *)
{
   using namespace fix_motionblur_names;

   void  * fix_motionblur_wthread1(void *arg);
   void  * fix_motionblur_wthread2(void *arg);

   float    rotate;
   int      ww3, hh3;
   int      ww8, hh8;
   int      mww, mhh;

   if (angle <= 90) rotate = angle;                                              //  avoid upside-down result
   else rotate = angle - 180;

   ww3 = E3pxm->ww;                                                              //  E3 size
   hh3 = E3pxm->hh;
   
   E8pxm = PXM_rotate(E3pxm,rotate);                                             //  E8 = E3 rotated so blur angle = 0
   E9pxm = PXM_copy(E8pxm);                                                      //  E8 = blurred input, E9 = sharp output

   do_wthreads(fix_motionblur_wthread1,NSMP);                                    //  replace black margins from edge pixels

   progress_setgoal(E8pxm->hh);                                                  //  progress monitor = rows

   do_wthreads(fix_motionblur_wthread2,NSMP);                                    //  worker thread, E8 --> sharpen --> E9

   progress_setgoal(0);

   PXM_free(E8pxm);                                                              //  discard input image
   E8pxm = 0;

   E8pxm = PXM_rotate(E9pxm,-rotate);                                            //  E8 = unrotated E9
   ww8 = E8pxm->ww;                                                              //  E8 size
   hh8 = E8pxm->hh;

   PXM_free(E9pxm);                                                              //  discard E9
   E9pxm = 0;
   
   mww = (ww8 - ww3) / 2;
   mhh = (hh8 - hh3) / 2;                                                        //  cut-off margins from two rotates
   PXM_free(E3pxm);
   E3pxm = PXM_copy_area(E8pxm,mww,mhh,ww3,hh3);                                 //  E8 - margins --> E3 (original size)

   PXM_free(E8pxm);                                                              //  discard E8
   E8pxm = 0;

   if (sa_stat == sa_stat_fini) sa_postfix();                                    //  if select area, restore unselected

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

   Fpaint2();
   return 0;                                                                     //  not executed, stop warning
}


//  propagate image edge pixels into the black margins created from rotation

void * fix_motionblur_wthread1(void *arg)
{
   using namespace fix_motionblur_names;

   int      index = *((int *) arg);
   int      px, py, pxL;
   float    *pix8, R, G, B;
   int      ww8 = E8pxm->ww;                                                     //  E8 size
   int      hh8 = E8pxm->hh;

   if (angle == 0) return 0;

   for (py = index; py < hh8; py += NSMP)                                        //  loop image rows
   {
      for (px = 0; px < ww8; px++)                                               //  loop columns
      {
         pix8 = PXMpix(E8pxm,px,py);
         if (pix8[0] > 0 || pix8[1] > 0 || pix8[2] > 0) break;                   //  find first non-black pixel in row
      }

      pxL = px + 2;                                                              //  use next pixel
      if (pxL < ww8) {
         pix8 = PXMpix(E8pxm,pxL,py);                                            //  get RGB values
         R = pix8[0];
         G = pix8[1];
         B = pix8[2];

         for (px = 0; px < pxL; px++) {                                          //  fill black pixels with RGB
            pix8 = PXMpix(E8pxm,px,py);
            pix8[0] = R;
            pix8[1] = G;
            pix8[2] = B;
         }
      }

      for (px = ww8-1; px > 0; px--)                                             //  find last non-black pixel in row
      {
         pix8 = PXMpix(E8pxm,px,py);
         if (pix8[0] > 0 || pix8[1] > 0 || pix8[2] > 0) break;
      }

      pxL = px - 2;                                                              //  use previous pixel
      if (pxL > 0) {
         pix8 = PXMpix(E8pxm,pxL,py);                                            //  get RGB values
         R = pix8[0];
         G = pix8[1];
         B = pix8[2];

         for (px = pxL+1; px < ww8; px++) {                                      //  fill black pixels with RGB
            pix8 = PXMpix(E8pxm,px,py);
            pix8[0] = R;
            pix8[1] = G;
            pix8[2] = B;
         }
      }
   }

   return 0;
}


//  fix motion blur worker thread function
//  E8 and E9 are rotated blurred image so that blur angle = 0
//  span = N: each blurred pixel is average of N input pixels
//  output = E9 = sharpened E8

void * fix_motionblur_wthread2(void *arg)
{
   using namespace fix_motionblur_names;

   void RLdecon(float *pixels, int Np, int Nd, int Nt);

   int      index = *((int *) arg);
   int      px, py, rgb;
   int      ww = E8pxm->ww, hh = E8pxm->hh, nc = E8pxm->nc;
   int      pcc = nc * sizeof(float);
   float    *pixx, *pix9, *RLval;
   float    *pix8, *pix8a, *pix8b;
   float    con, F1, F2;

   if (span < 1) return 0;

   RLval = (float *) zmalloc(ww * sizeof(float),"motionblur-RL");

   for (py = index; py < hh; py += NSMP)                                         //  loop image rows
   {
      progress_addvalue(index,1);                                                //  count progress

      for (rgb = 0; rgb < 3; rgb++)                                              //  loop RGB                  bugfix      24.60
      {
         for (px = 0; px < ww; px++)                                             //  loop pixels in row
         {
            pix9 = PXMpix(E8pxm,px,py);                                          //  one RGB value per pixel
            RLval[px] = pix9[rgb];
         }

         RLdecon(RLval,ww,span,iter);                                            //  do R-L algorithm on pixel array

         for (px = 0; px < ww; px++)
         {
            pix9 = PXMpix(E9pxm,px,py);                                          //  save revised RGB values
            pix9[rgb] = RLval[px];
         }
      }

      for (px = ww-1; px > span; px--)                                           //  fix position shift
      {
         pix9 = PXMpix(E9pxm,px,py);
         pixx = PXMpix(E9pxm,px-span/2,py);
         memcpy(pix9,pixx,pcc);
      }

      for (px = 1; px < ww-1; px++)                                              //  suppress ringing
      for (rgb = 0; rgb < 3; rgb++)
      {
         pix8 = PXMpix(E8pxm,px,py);                                             //  blurred input image
         pix8a = pix8 - nc;
         pix8b = pix8 + nc;
         con = span * 0.0039 * fabsf(pix8a[rgb] - pix8b[rgb]);                   //  0 - 1 = max. contrast
         if (con > 1.0) con = 1.0;
         F1 = 0.1 * supring;                                                     //  0 - 1 = max. suppression
         F2 = F1 * (1.0 - con);                                                  //  min. contrast --> max. suppression
         pix9 = PXMpix(E9pxm,px,py);                                             //  sharpened output image
         pix9[rgb] = F2 * pix8[rgb] + (1.0 - F2) * pix9[rgb];
      }
   }

   zfree(RLval);
   return 0;                                                                     //  exit thread
}


/***

   Richardson-Lucy deconvolution for special case: linear uniform blur.
      pixels[]       row of motion blurred pixels in blur direction (one RGB channel)
      Np             pixel row length
      Nd             blur span: Nd pixels contribute equally to each blurred pixel
                                (inclusive: Nd = 1 means no blur)
      Nt             algorithm iterations
   Variable names follow Wikipedia article on Richardson-Lucy deconvolution.

***/

void RLdecon(float *pixels, int Np, int Nd, int Nt)
{
   int      ii, jj, tt;
   float    Pij = 1.0 / Nd;                                                      //  pixel contribution factor
   float    *Ci = (float *) malloc(Np * sizeof(float));                          //  Ci factor per pixel
   float    *Uj = (float *) malloc(Np * sizeof(float));                          //  old/new estimated value per pixel

   for (jj = 0; jj < Np; jj++)                                                   //  initial Uj per pixel jj
      Uj[jj] = pixels[jj];

   for (tt = 0; tt < Nt; tt++)                                                   //  algorithm iterations
   {
      for (ii = Nd; ii < Np-Nd; ii++)                                            //  compute Ci per pixel ii
      {
         Ci[ii] = 0;
         for (jj = 0; jj < Nd; jj++)                                             //  Nd pixels contributing to ii
            Ci[ii] += Pij * Uj[ii-jj];
         if (Ci[ii] <= 0) Ci[ii] = 1;
      }

      for (jj = Nd; jj < Np-Nd-Nd; jj++)                                         //  compute new Uj per pixel jj
      {
         float S = 0;
         for (ii = 0; ii < Nd; ii++)                                             //  Nd pixels contributing to pixel jj
            S += pixels[jj+ii] / Ci[jj+ii] * Pij;
         Uj[jj] = Uj[jj] * S;                                                    //  new Uj replaces old Uj
      }
   }

   for (jj = 0; jj < Np; jj++)
   {
      pixels[jj] = Uj[jj];
      if (pixels[jj] < 0) pixels[jj] = 0;
      if (pixels[jj] > 255) pixels[jj] = 255;
   }

   free(Ci);
   free(Uj);
   return;
}


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

//  image noise reduction                                                        //  overhauled  24.60

namespace denoise_names
{
   cch      *dn_method;
   int      dn_radius, dn_limit;
   int      noise_histogram[3][256];
   ch       edit_hist[200];

   zdialog     *zd_denoise_measure;
   GtkWidget   *denoise_measure_drawwin;
   editfunc    EFdenoise;
}


//  menu function

void m_denoise(GtkWidget *, ch *menu)
{
   using namespace denoise_names;

   int    denoise_dialog_event(zdialog *zd, ch *event);
   void   denoise_characterize();
   void * denoise_thread(void *);

   ch    *tip = "Apply repeatedly while watching the image.";

   F1_help_topic = "denoise";

   Plog(1,"m_denoise \n");

   EFdenoise.menuname = "Denoise";
   EFdenoise.menufunc = m_denoise;
   EFdenoise.Farea = 2;                                                          //  select area usable
   EFdenoise.Fpaintedits = 1;                                                    //  use with paint edits OK
   EFdenoise.Fscript = 1;                                                        //  scripting supported
   EFdenoise.threadfunc = denoise_thread;                                        //  thread function

   if (! edit_setup(EFdenoise)) return;                                          //  setup edit
   
   E8pxm = PXM_copy(E0pxm);                                                      //  initial input image

/***
          __________________________________________________
         |               Noise Reduction                    |
         |   Apply repeatedly while watching the image.     |
         |                                                  |
         |  Radius [ 2 ]  scope of neighbor pixels          |
         |  Limit  [ 13 ] ignore differences > this level   |
         |                                                  |
         |  [median]  pixel = median of all neighbors       |
         |  [smooth]  pixel = mean of best match neighbors  |
         |                                                  | 
         |                      [Measure] [Reset] [OK] [X]  |
         |__________________________________________________|

***/

   zdialog *zd = zdialog_new("Noise Reduction",Mwin,"Measure","Reset","OK"," X ",null);
   EFdenoise.zd = zd;

   zdialog_add_widget(zd,"label","labtip","dialog",tip,"space=3");

   zdialog_add_widget(zd,"hsep","sep","dialog",0,"space=5");

   zdialog_add_widget(zd,"hbox","hbrad","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labrad","hbrad","Radius","space=3");
   zdialog_add_widget(zd,"zspin","dn_radius","hbrad","1|5|1|1","size=2|space=3");
   zdialog_add_widget(zd,"label","labrad","hbrad","scope of neighbor pixels","space=3");

   zdialog_add_widget(zd,"hbox","hblimit","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","lablimit","hblimit","Limit","space=3");
   zdialog_add_widget(zd,"zspin","dn_limit","hblimit","1|255|1|100","size=4|space=3");
   zdialog_add_widget(zd,"label","lablimit","hblimit","ignore differences > this level","space=3");
   
   zdialog_add_widget(zd,"hsep","sep","dialog",0,"space=8");

   zdialog_add_widget(zd,"hbox","hbmed","dialog",0,"space=1");
   zdialog_add_widget(zd,"button","median","hbmed"," median ","space=3");
   zdialog_add_widget(zd,"label","labmed","hbmed","pixel = median of all neighbors","space=3");

   zdialog_add_widget(zd,"hbox","hbsmoo","dialog",0,"space=1");
   zdialog_add_widget(zd,"button","smooth","hbsmoo"," smooth ","space=3");
   zdialog_add_widget(zd,"label","labsmo","hbsmoo","pixel = mean of best match neighbors","space=3");
   
   zdialog_load_inputs(zd);
   
   denoise_characterize();                                                       //  characterize image noise
   zdialog_stuff(zd,"dn_limit",dn_limit);                                        //    = default limit

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


//  dialog event and completion callback function

int denoise_dialog_event(zdialog * zd, ch *event)                                //  reworked for script files
{
   using namespace denoise_names;

   void   denoise_measure();

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

   zdialog_fetch(zd,"dn_radius",dn_radius);
   zdialog_fetch(zd,"dn_limit",dn_limit);
   
   if (strstr("median smooth",event)) {
      if (strmatch(event,"median")) dn_method = "median";
      if (strmatch(event,"smooth")) dn_method = "smooth";
      thread_signal();
      return 1;
   }

   if (strmatch(event,"paint")) {                                                //  mouse paint
      thread_signal();
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  [measure]
         zd->zstat = 0;                                                          //  keep dialog active
         denoise_measure();
         return 1;
      }

      if (zd->zstat == 2) {                                                      //  [reset]
         PXM_copy(E0pxm,E8pxm);                                                  //  restore input image
         edit_reset();                                                           //  undo edits
         zd->zstat = 0;                                                          //  keep dialog active
         return 1;
      }

      if (zd->zstat == 3) {                                                      //  [OK]  commit edit
         edit_addhist(edit_hist);                                                //  edit params > edit hist
         edit_done(0);
      }

      else edit_cancel(0);                                                       //  discard edit

      PXM_free(E8pxm);                                                           //  free input image

      if (zd_denoise_measure) {                                                  //  kill measure dialog
         freeMouse();
         zdialog_free(zd_denoise_measure);
         zd_denoise_measure = 0;
      }
      
      return 1;
   }

   return 1;
}


//  characterize noise levels
//  compute noise histogram using neighboring pixel differences
//  lower buckets (from most uniform areas) will indicate true noise

void denoise_characterize()
{
   using namespace denoise_names;

   int      px, py, diff, rgb, ii, cc, skip, Npix, Tpix;
   double   val, sum, sum2, mean, sigma, varnc, thresh, limit;
   float    *pix1, *pix2;

   Funcbusy(+1,1);

   for (rgb = 0; rgb < 3; rgb++)                                                 //  clear histogram
   for (ii = 0; ii < 256; ii++)
      noise_histogram[rgb][ii] = 0;
   
   cc = Eww * Ehh;                                                               //  image size
   skip = sqrt(cc / 1000.0);                                                     //  sample 1000 pixels
   if (skip < 1) skip = 1;

   for (py = 0; py < Ehh; py += skip)                                            //  loop image pixels
   for (px = 0; px < Eww-1; px += skip)                                          //  omit last column
   {
      pix1 = PXMpix(E1pxm,px,py);                                                //  pixel
      pix2 = PXMpix(E1pxm,px+1,py);                                              //  neighbor on right

      for (rgb = 0; rgb < 3; rgb++)
      {
         diff = pix1[rgb] - pix2[rgb];                                           //  pixel-pixel RGB difference
         diff = abs(diff);
         noise_histogram[rgb][diff]++;
      }
   }

   thresh = 0;
   limit = 200;                                                                  //  ignore pixel diffs > 200

   for (rgb = 0; rgb < 3; rgb++)                                                 //  get mean and sigma per RGB color
   {
      sum = sum2 = Tpix = 0;

      for (ii = 0; ii < limit; ii++) 
      {
         Npix = noise_histogram[rgb][ii];
         Tpix += Npix;
         val = ii * Npix;
         sum += val;
         sum2 += val * val;
      }

      mean = sum / limit;
      varnc = (sum2 - 2.0 * mean * mean) / limit + mean * mean;
      sigma = sqrtf(varnc);

      mean = mean / Tpix * limit;                                                //  mean and sigma
      sigma = sigma / Tpix * limit;
      if (sigma > thresh) thresh = sigma;                                        //  keep highest RGB
   }

   sigma = thresh;                                                               //  greatest RGB sigma
   dn_limit = sigma + 0.5;

   Plog(1,"RGB noise sigma: %.0f \n",sigma);

   Funcbusy(-1);
   return;
}


//  image noise reduction thread

void * denoise_thread(void *)
{
   using namespace denoise_names;

   void * denoise_median_wthread(void *arg);
   void * denoise_smooth_wthread(void *arg);
   
   if (Fpaintedits && ! Mdrag) return 0;                                         //  if mouse paint, must be drag

   if (strmatch(dn_method,"median"))
   {
      if (sa_stat == sa_stat_fini) progress_setgoal(sa_Npixel);                  //  initz. progress counter
      else  progress_setgoal(Eww * Ehh);

      get_edit_pixels_init(NSMP,dn_radius);                                      //  initz. pixel loop
      do_wthreads(denoise_median_wthread,NSMP);                                  //  median denoise
      PXM_copy(E3pxm,E8pxm);                                                     //  E3 (output) --> E8 (input for next)
      snprintf(edit_hist,200,"median rad:%d",dn_radius);                         //  metadata edit params > edit hist
   }

   if (strmatch(dn_method,"smooth"))
   {
      if (sa_stat == sa_stat_fini) progress_setgoal(sa_Npixel);                  //  initz. progress counter
      else  progress_setgoal(Eww * Ehh);

      get_edit_pixels_init(NSMP,dn_radius);                                      //  initz. pixel loop
      do_wthreads(denoise_smooth_wthread,NSMP);                                  //  smooth denoise
      PXM_copy(E3pxm,E8pxm);                                                     //  E3 (output) --> E8 (input for next)
      snprintf(edit_hist,200,"smooth rad:%d",dn_radius);                         //  metadata edit params > edit hist
   }

   progress_setgoal(0);

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

   if (! Fpaintedits) Fpaint2();                                                 //  update window
   return 0;
}


//  Median:
//  Use median RGB brightness for pixels within radius

void * denoise_median_wthread(void *arg)
{
   using namespace denoise_names;

   int         index = *((int *) arg);
   int         ii, rgb, Fend;
   int         px, py, dx, dy, rad, Frad;
   int         nc = E3pxm->nc;
   float       *pix8, *pix3, *pixN, pix9[3];
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       blend, median;
   int16       rgbhist[256];
   int         rgbsum;

   rad = dn_radius;

   Frad = 2 * rad + 1;                                                           //  get sample area
   Frad = Frad * Frad;

   while (true)
   {
      if (Fescape) return 0;                                                     //  user kill

      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) break;
      if (blend == 0) continue;

      pix8 = PXMpix(E8pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      R1 = pix9[0] = pix8[0];
      G1 = pix9[1] = pix8[1];
      B1 = pix9[2] = pix8[2];

      R3 = pix3[0];
      G3 = pix3[1];
      B3 = pix3[2];

      for (rgb = 0; rgb < 3; rgb++)                                              //  loop all RGB colors
      {
         memset(rgbhist,0,256*sizeof(int16));                                    //  clear rgb histogram
         rgbsum = 0;

         for (dy = -rad; dy <= rad; dy++)                                        //  loop surrounding pixels
         for (dx = -rad; dx <= rad; dx++)                                        //  get RGB values
         {
            pixN = pix8 + (dy * Eww + dx) * nc;                                  //  make histogram of RGB values
            ii = pixN[rgb];
            rgbhist[ii]++;
         }

         for (ii = 0; ii < 256; ii++)                                            //  sum histogram from 0 to 255
         {
            rgbsum += rgbhist[ii];                                               //  break when half of RGB values
            if (rgbsum > Frad / 2) break;                                        //    have been counted
         }
         
         median = ii;
         
         if (fabsf(pix8[rgb] - median) < dn_limit)                               //  if | rgb - median | < threshold
            pix9[rgb] = median;                                                  //    moderate rgb
      }

      R9 = pix9[0];
      G9 = pix9[1];
      B9 = pix9[2];

      if (Fpaintedits)                                                           //  gradual edit within mouse circle
      {
         if (blend > 0)
         {                                                                       //  increase edit
            R3 = blend * R9 + (1-blend) * R3;
            G3 = blend * G9 + (1-blend) * G3;
            B3 = blend * B9 + (1-blend) * B3;
         }

         else if (blend < 0)
         {                                                                       //  decrease edit
            R3 = -blend * R1 + (1+blend) * R3;
            G3 = -blend * G1 + (1+blend) * G3;
            B3 = -blend * B1 + (1+blend) * B3;
         }
      }

      else                                                                       //  full edit for image or area
      {
         R3 = blend * R9 + (1-blend) * R1;
         G3 = blend * G9 + (1-blend) * G1;
         B3 = blend * B9 + (1-blend) * B1;
      }

      pix3[0] = R3;                                                              //  set output pixel
      pix3[1] = G3;
      pix3[2] = B3;

      progress_addvalue(index,1);                                                //  track progress
   }

   return 0;
}


//  smooth:
//  For each pixel, find two best-match opposing neighbors.
//  Set pixel RGB values to mean of best neighbor RGB values.

void * denoise_smooth_wthread(void *arg)
{
   using namespace denoise_names;

   int  denoise_smooth_compfunc(ch *rec1, ch *rec2);

   int         index = *((int *) arg);
   int         Fend, rad, rgb;
   int         px, py, px1, py1, px2, py2;
   float       *pix8, *pix3;
   float       *pixA, *pixB;
   float       *pix91, *pix92, pix9[3];
   float       blend;
   int         bmpx, bmpy;
   float       diffA, diffB, diffAB, mindiff, mrgb;

   rad = dn_radius;

   while (true)                                                                  //  loop all image pixels
   {
      if (Fescape) return 0;                                                     //  user kill

      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) break;
      if (blend == 0) continue;

      pix8 = PXMpix(E8pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      for (rgb = 0; rgb < 3; rgb++)                                              //  loop each RGB color
      {
         mindiff = 999;                                                          //  remember best matching pixel pair
         bmpx = bmpy = 0;

         py1 = -rad;                                                             //  py1 = top row
         py2 = +rad;                                                             //  py2 = bottom row

         for (px1 = -rad; px1 <= +rad; px1++)                                    //  px1/py1 = top row pixel
         {
            px2 = -px1;                                                          //  px2/py2 = opposite bottom row pixel

            pixA = PXMpix(E8pxm,px+px1,py+py1);                                  //  top row pixel
            pixB = PXMpix(E8pxm,px+px2,py+py2);                                  //  bottom row pixel

            diffA = fabsf(pixA[rgb] - pix8[rgb]);                                //  diff from pix8
            diffB = fabsf(pixB[rgb] - pix8[rgb]);                                //  diff from pix8

            diffAB = diffA + diffB;                                              //  match both with px/py
            if (diffAB < mindiff) {
               mindiff = diffAB;                                                 //  remember best match
               bmpx = px1;
               bmpy = py1;
            }
         }

         px1 = -rad;                                                             //  px1 = left col
         px2 = +rad;                                                             //  px2 = right col

         for (py1 = -rad; py1 <= +rad; py1++)                                    //  px1/py1 = left col
         {
            py2 = -py1;                                                          //  px2/py2 = opposite on right col

            pixA = PXMpix(E8pxm,px+px1,py+py1);                                  //  left col pixel
            pixB = PXMpix(E8pxm,px+px2,py+py2);                                  //  right col pixel

            diffA = fabsf(pixA[rgb] - pix8[rgb]);                                //  diff from pix8
            diffB = fabsf(pixB[rgb] - pix8[rgb]);                                //  diff from pix8

            diffAB = fabsf(pixA[rgb] - pixB[rgb]);                               //  test difference

            diffAB = diffA + diffB;                                              //  match both with px/py
            if (diffAB < mindiff) {
               mindiff = diffAB;                                                 //  remember best match
               bmpx = px1;
               bmpy = py1;
            }
         }

         px1 = bmpx;                                                             //  best matching pixel pair
         py1 = bmpy;                                                             //  (relative to px/py)
         px2 = -px1;
         py2 = -py1;

         px1 += px;                                                              //  absolute values
         py1 += py;
         px2 += px;
         py2 += py;

         pix91 = PXMpix(E8pxm,px1,py1);                                          //  opposing pixel pair
         pix92 = PXMpix(E8pxm,px2,py2);
         mrgb = 0.5 * (pix91[rgb] + pix92[rgb]);                                 //  mean RGB value

         if (fabsf(pix8[rgb] - mrgb) < dn_limit) pix9[rgb] = mrgb;               //  if | rgb - mean | < threshold
         else pix9[rgb] = pix8[rgb];                                             //    moderate rgb

         if (Fpaintedits) {                                                      //  gradual edit within mouse circle
            if (blend > 0)
               pix3[rgb] = blend * pix9[rgb] + (1-blend) * pix3[rgb];
            else if (blend < 0)
               pix3[rgb] = -blend * pix8[rgb] + (1+blend) * pix3[rgb];
         }

         else                                                                    //  full edit for image or area
            pix3[rgb] = blend * pix9[rgb] + (1-blend) * pix8[rgb];

      }

      progress_addvalue(index,1);                                                //  track progress
   }

   return 0;
}


//  dialog to measure noise at mouse position

void denoise_measure()
{
   using namespace denoise_names;

   int denoise_measure_dialog_event(zdialog *zd, ch *event);

   GtkWidget   *frdraw, *drawwin;
   ch          text[100];
   ch          *title = "Measure Noise";
   ch          *mousemess = "Move mouse in a monotone image area.";
   ch          *mformat = "  mean RGB:  %5.0f %5.0f %5.0f ";
   ch          *nformat = " mean noise: %5.2f %5.2f %5.2f ";

/***
          _______________________________________
         |           Measure Noise               |
         |                                       |
         |  Move mouse in a monotone image area. |
         | _____________________________________ |
         ||                                     ||
         ||                                     ||
         ||.....................................||
         ||                                     ||
         ||                                     ||
         ||_____________________________________||       drawing area
         ||                                     ||
         ||                                     ||
         ||.....................................||
         ||                                     ||
         ||_____________________________________||
         | center                           edge |
         |                                       |
         |   mean RGB:   100   150   200         |
         |  mean noise:  1.51  1.23  0.76        |
         |                                       |
         |                                   [X] |
         |_______________________________________|

***/

   if (zd_denoise_measure) return;

   zdialog *zd = zdialog_new(title,Mwin," X ",null);                             //  measure noise dialog
   zd_denoise_measure = zd;

   zdialog_add_widget(zd,"label","clab","dialog",mousemess,"space=5");

   zdialog_add_widget(zd,"frame","frdraw","dialog",0,"expand");                  //  frame for drawing areas
   frdraw = zdialog_gtkwidget(zd,"frdraw");
   drawwin = gtk_drawing_area_new();                                             //  drawing area
   gtk_container_add(GTK_CONTAINER(frdraw),drawwin);
   denoise_measure_drawwin = drawwin;

   zdialog_add_widget(zd,"hbox","hbce","dialog");
   zdialog_add_widget(zd,"label","labcen","hbce","Center","space=3");
   zdialog_add_widget(zd,"label","space","hbce",0,"expand");
   zdialog_add_widget(zd,"label","labend","hbce","Edge","space=5");

   snprintf(text,100,mformat,0.0,0.0,0.0);                                       //  mean RGB:     0     0     0
   zdialog_add_widget(zd,"label","mlab","dialog",text);
   snprintf(text,100,nformat,0.0,0.0,0.0);                                       //  mean noise:  0.00  0.00  0.00
   zdialog_add_widget(zd,"label","nlab","dialog",text);

   zdialog_resize(zd,300,300);
   zdialog_run(zd,denoise_measure_dialog_event,"save");                          //  run dialog
   return;
}


//  dialog event and completion function

int denoise_measure_dialog_event(zdialog *zd, ch *event)
{
   using namespace denoise_names;

   void denoise_measure_mousefunc();

   if (strmatch(event,"focus"))
      takeMouse(denoise_measure_mousefunc,dragcursor);                           //  connect mouse function

   if (zd->zstat) {
      freeMouse();                                                               //  free mouse
      zdialog_free(zd);
      zd_denoise_measure = 0;
   }

   return 1;
}


//  mouse function
//  sample noise where the mouse is clicked
//  assumed: mouse is on a monotone image area

void denoise_measure_mousefunc()
{
   using namespace denoise_names;

   GtkWidget   *drawwin;
   GdkWindow   *gdkwin;
   cairo_t     *cr;
   zdialog     *zd = zd_denoise_measure;
   ch          *mformat = "  mean RGB:  %5.0f %5.0f %5.0f ";
   ch          *nformat = " mean noise: %5.2f %5.2f %5.2f ";

   ch          text[100];
   int         mx, my, px, py, qx, qy, Npix;
   float       *pix3, R;
   float       Rm, Gm, Bm, Rn, Gn, Bn, Ro, Go, Bo;
   int         dww, dhh;
   float       max, xscale, yscale;
   float       rx, ry;
   float       Noise[400][3];
   double      dashes[2] = { 1, 3 };

   if (! E3pxm) return;
   if (! zd) return;
   if (Nfuncbusy) return;                                                        //  wait if denoise in progress

   mx = Mxposn;                                                                  //  mouse position
   my = Myposn;

   if (mx < 13 || mx >= Eww-13) return;                                          //  must be 12+ pixels from image edge
   if (my < 13 || my >= Ehh-13) return;

   draw_mousecircle(Mxposn,Myposn,10,0,0);                                       //  draw mouse circle, radius 10

   Npix = 0;
   Rm = Gm = Bm = 0;

   for (py = my-10; py <= my+10; py++)                                           //  loop pixels within mouise circle
   for (px = mx-10; px <= mx+10; px++)                                           //  (approx. 314 pixels)
   {
      R = sqrtf((px-mx)*(px-mx) + (py-my)*(py-my));                              //  distance from center
      if (R > 10) continue;

      pix3 = PXMpix(E3pxm,px,py);                                                //  get pixel RGB values
      Rm += pix3[0];                                                             //  accumulate
      Gm += pix3[1];
      Bm += pix3[2];

      Npix++;
   }

   Rm = Rm / Npix;                                                               //  mean RGB values
   Gm = Gm / Npix;                                                               //    for pixels within mouse
   Bm = Bm / Npix;

   Npix = 0;
   Rn = Gn = Bn = 0;

   for (py = my-10; py <= my+10; py++)                                           //  loop pixels within mouise circle
   for (px = mx-10; px <= mx+10; px++)                                           //  (approx. 314 pixels)
   {
      R = sqrtf((px-mx)*(px-mx) + (py-my)*(py-my));                              //  distance from center
      if (R > 10) continue;

      Ro = Go = Bo = 0;

      for (qy = py-2; qy <= py+2; qy++)                                          //  for each pixel, get mean RGB
      for (qx = px-2; qx <= px+2; qx++)                                          //    for 5x5 surrounding pixels
      {
         pix3 = PXMpix(E3pxm,qx,qy);
         Ro += pix3[0];
         Go += pix3[1];
         Bo += pix3[2];
      }

      Ro = Ro / 25;                                                              //  mean RGB for surrounding pixels
      Go = Go / 25;
      Bo = Bo / 25;

      pix3 = PXMpix(E3pxm,px,py);                                                //  get pixel RGB noise levels

      Noise[Npix][0] = pix3[0] - Ro;                                             //  noise = pixel value - mean
      Noise[Npix][1] = pix3[1] - Go;
      Noise[Npix][2] = pix3[2] - Bo;

      Rn += fabsf(Noise[Npix][0]);                                               //  accumulate absolute values
      Gn += fabsf(Noise[Npix][1]);
      Bn += fabsf(Noise[Npix][2]);

      Npix++;
   }

   Rn = Rn / Npix;                                                               //  mean RGB noise levels
   Gn = Gn / Npix;                                                               //    for pixels within mouse
   Bn = Bn / Npix;

   snprintf(text,100,mformat,0.0,0.0,0.0);                                       //  clear dialog data
   zdialog_stuff(zd,"mlab",text);
   snprintf(text,100,nformat,0.0,0.0,0.0);
   zdialog_stuff(zd,"nlab",text);

   snprintf(text,100,mformat,Rm,Gm,Bm);                                          //  mean RGB:   NNN   NNN   NNN
   zdialog_stuff(zd,"mlab",text);

   snprintf(text,100,nformat,Rn,Gn,Bn);                                          //  mean noise:  N.NN  N.NN  N.NN
   zdialog_stuff(zd,"nlab",text);

   max = Rn;
   if (Gn > max) max = Gn;
   if (Bn > max) max = Bn;

   drawwin = denoise_measure_drawwin;
   gdkwin = gtk_widget_get_window(drawwin);                                      //  GDK drawing window

   dww = gtk_widget_get_allocated_width(drawwin);                                //  drawing window size
   dhh = gtk_widget_get_allocated_height(drawwin);

   xscale = dww / 10.0;                                                          //  x scale:  0 to max radius
   yscale = dhh / 20.0;                                                          //  y scale: -10 to +10 noise level

   cr = draw_context_create(gdkwin,draw_context);

   cairo_set_source_rgb(cr,1,1,1);                                               //  white background
   cairo_paint(cr);

   cairo_set_source_rgb(cr,0,0,0);                                               //  paint black

   cairo_set_line_width(cr,2);                                                   //  center line
   cairo_set_dash(cr,dashes,0,0);
   cairo_move_to(cr,0,0.5*dhh);
   cairo_line_to(cr,dww,0.5*dhh);
   cairo_stroke(cr);

   cairo_set_dash(cr,dashes,2,0);                                                //  dash lines at -5 and +5
   cairo_move_to(cr,0,0.25*dhh);
   cairo_line_to(cr,dww,0.25*dhh);
   cairo_move_to(cr,0,0.75*dhh);
   cairo_line_to(cr,dww,0.75*dhh);
   cairo_stroke(cr);

   cairo_set_source_rgb(cr,1,0,0);

   Npix = 0;

   for (py = my-10; py <= my+10; py++)                                           //  loop pixels within mouise circle
   for (px = mx-10; px <= mx+10; px++)                                           //  (approx. 314 pixels)
   {
      R = sqrtf((px-mx)*(px-mx) + (py-my)*(py-my));                              //  distance from center
      if (R > 10) continue;
      Rn = Noise[Npix][0];                                                       //  RED noise
      rx = R * xscale;                                                           //  px, 0 to dww
      ry = 0.5 * dhh - Rn * yscale;                                              //  red py, 0 to dhh
      cairo_move_to(cr,rx,ry);
      cairo_arc(cr,rx,ry,1,0,2*PI);
      Npix++;
   }

   cairo_stroke(cr);

   cairo_set_source_rgb(cr,0,1,0);

   Npix = 0;

   for (py = my-10; py <= my+10; py++)                                           //  same for GREEN noise
   for (px = mx-10; px <= mx+10; px++)
   {
      R = sqrtf((px-mx)*(px-mx) + (py-my)*(py-my));
      if (R > 10) continue;
      Gn = Noise[Npix][1];
      rx = R * xscale;
      ry = 0.5 * dhh - Gn * yscale;
      cairo_move_to(cr,rx,ry);
      cairo_arc(cr,rx,ry,1,0,2*PI);
      Npix++;
   }

   cairo_stroke(cr);

   cairo_set_source_rgb(cr,0,0,1);

   Npix = 0;

   for (py = my-10; py <= my+10; py++)                                           //  same for BLUE noise
   for (px = mx-10; px <= mx+10; px++)
   {
      R = (px-mx)*(px-mx) + (py-my)*(py-my);
      if (R > 100) continue;
      R = 0.1 * R;
      Bn = Noise[Npix][2];
      rx = R * xscale;
      ry = 0.5 * dhh - Bn * yscale;
      cairo_move_to(cr,rx,ry);
      cairo_arc(cr,rx,ry,1,0,2*PI);
      Npix++;
   }

   cairo_stroke(cr);

   draw_context_destroy(draw_context);
   return;
}


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

//  Defog
//  remove or add fog/haze to an image or selected areas

namespace defog_names
{
   editfunc    EFdefog;
   float       blue;                                                             //  blue attenuation, 0 ... 1
   float       bright;                                                           //  brightness addition, 0 ... 1
}


//  menu function

void m_defog(GtkWidget *, ch *menu)
{
   using namespace defog_names;

   int    defog_dialog_event(zdialog* zd, ch *event);
   void   defog_curvedit(int spc);
   void * defog_thread(void *);

   GtkWidget   *drawwin_scale;

   F1_help_topic = "defog";

   Plog(1,"m_defog \n");

   EFdefog.menuname = "Defog";
   EFdefog.menufunc = m_defog;
   EFdefog.FprevReq = 1;                                                         //  use preview
   EFdefog.Farea = 2;                                                            //  select area usable
   EFdefog.Fpaintedits = 1;                                                      //  use with paint edits OK
   EFdefog.Fscript = 1;                                                          //  scripting supported
   EFdefog.threadfunc = defog_thread;

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

/***
       ____________________________________________
      |                  Defog                     |
      |             Reduce fog/haze:               |
      |  ________________________________________  |
      | |                                        | |
      | |                                        | |
      | |                                        | |                             //  editable curve area
      | | -------------------------------------- | |                             //  X = pixel brightness, Y = fog/haze
      | |                                        | |
      | |                                        | |
      | |________________________________________| |
      | |________________________________________| |                             //  brightness scale: black --> white
      |                                            |
      |  + bright =================[]============  |
      |  - blue  =============[]=================  |
      |                                            |
      |                           [Reset] [OK] [X] |
      |____________________________________________|

***/

   zdialog *zd = zdialog_new("Reduce Fog/Haze",Mwin,"Reset","OK"," X ",null);
   EFdefog.zd = zd;

   zdialog_add_widget(zd,"frame","frameH","dialog",0,"expand");                  //  edit-curve
   zdialog_add_widget(zd,"frame","frameB","dialog",0,"space=5");                 //  brightness scale

   zdialog_add_widget(zd,"hsep","hsep","dialog",0,"space=8");

   zdialog_add_widget(zd,"hbox","hbbrite","dialog");                             //  brightness slider
   zdialog_add_widget(zd,"label","labbrite","hbbrite","+ bright","space=8");
   zdialog_add_widget(zd,"hscale","bright","hbbrite","0|1|0.01|0","expand");     //  0 ... 1

   zdialog_add_widget(zd,"hbox","hbblue","dialog");                              //  reduce blue slider
   zdialog_add_widget(zd,"label","labblue","hbblue","‒ blue","space=8");
   zdialog_add_widget(zd,"hscale","blue","hbblue","0|1|0.01|0","expand");        //  0 ... 1

   GtkWidget *frameH = zdialog_gtkwidget(zd,"frameH");                           //  setup edit curve
   spldat *sd = splcurve_init(frameH,defog_curvedit);
   EFdefog.sd = sd;

   sd->Nscale = 0;                                                               //  no scale lines

   sd->nap[0] = 3;                                                               //  initial curve, 3 nodes
   sd->vert[0] = 0;
   sd->apx[0][0] = 0.01;                                                         //  horizontal line
   sd->apy[0][0] = 0.01;
   sd->apx[0][1] = 0.50;
   sd->apy[0][1] = 0.01;                                                         //  curve 0 = defog value
   sd->apx[0][2] = 0.99;
   sd->apy[0][2] = 0.01;
   splcurve_generate(sd,0);
   sd->mod[0] = 0;                                                               //  mark curve unmodified

   sd->Nspc = 1;                                                                 //  1 curve
   sd->fact[0] = 1;                                                              //  curve 0 active

   GtkWidget *frameB = zdialog_gtkwidget(zd,"frameB");                           //  setup brightness scale drawing area
   drawwin_scale = gtk_drawing_area_new();
   gtk_container_add(GTK_CONTAINER(frameB),drawwin_scale);
   gtk_widget_set_size_request(drawwin_scale,100,12);
   G_SIGNAL(drawwin_scale,"draw",brightness_scale,0);

   blue = 0;                                                                     //  no blue attenuation
   bright = 0;                                                                   //  no brightness addition

   zdialog_resize(zd,350,250);
   zdialog_run(zd,defog_dialog_event,"save");                                    //  run dialog - parallel

   return;
}


//  dialog event and completion callback function

int defog_dialog_event(zdialog *zd, ch *event)
{
   using namespace defog_names;

   spldat      *sd = EFdefog.sd;
   float       Fapply = 0;

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  apply and quit
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  cancel

   if (strmatch(event,"apply")) {                                                //  from script                           24.30
      zdialog_fetch(zd,"blue",blue);                                             //  get dialog inputs
      zdialog_fetch(zd,"bright",bright);
      Fapply = 1;
      zd->zstat = 2;
   }
   
   if (strmatch(event,"focus")) return 1;

   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();                                                           //  get full size image
      thread_signal();
      thread_wait();                                                             //  required for paint edits
      return 1;
   }

   if (strmatch(event,"blue")) zdialog_fetch(zd,"blue",blue);                    //  get dialog inputs
   if (strmatch(event,"bright")) zdialog_fetch(zd,"bright",bright);

   if (zstrstr("blue bright",event)) Fapply = 1;

   if (strmatch(event,"paint"))  Fapply = 1;                                     //  mouse paint

   if (Fapply) thread_signal();                                                  //  update the image

   if (zd->zstat == 1)                                                           //  [reset]
   {
      zd->zstat = 0;                                                             //  keep dialog active

      zdialog_stuff(zd,"blue",0);
      zdialog_stuff(zd,"bright",0);
      blue = bright = 0;

      sd->nap[0] = 3;                                                            //  defog curve is neutral
      sd->vert[0] = 0;
      sd->apx[0][0] = 0.01;
      sd->apy[0][0] = 0.01;
      sd->apx[0][1] = 0.50;
      sd->apy[0][1] = 0.01;
      sd->apx[0][2] = 0.99;
      sd->apy[0][2] = 0.01;
      splcurve_generate(sd,0);
      sd->mod[0] = 0;                                                            //  mark curve unmodified

      gtk_widget_queue_draw(sd->drawarea);                                       //  redraw curves

      edit_reset();
      return 1;
   }

   if (zd->zstat == 2)                                                           //  [OK]
   {
      freeMouse();
      edit_fullsize();                                                           //  get full size image
      thread_signal();                                                           //  apply changes
      edit_done(0);                                                              //  complete edit
      return 1;
   }

   if (zd->zstat < 0 || zd->zstat > 2) {                                         //  cancel
      edit_cancel(0);
      return 1;
   }

   return 1;
}


//  this function is called when a curve is edited

void defog_curvedit(int spc)
{
   using namespace defog_names;
   thread_signal();
   return;
}


//  thread function

void * defog_thread(void *arg)
{
   using namespace defog_names;

   void * defog_wthread(void *);

   if (Fpaintedits && ! Mdrag) return 0;                                         //  if mouse paint, must be drag

   get_edit_pixels_init(NSMP,0);                                                 //  initz. pixel loop

   do_wthreads(defog_wthread,NSMP);

   CEF->Fmods++;                                                                 //  image3 modified
   CEF->Fsaved = 0;

   if (! Fpaintedits) Fpaint2();                                                 //  update window
   return 0;
}


void * defog_wthread(void *arg)                                                  //  worker thread function
{
   using namespace defog_names;

   int         index = *((int *) arg);
   int         ii, Fend, px, py;
   float       *pix1, *pix3;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       pixB1, pixB2, defog, R;
   float       coeff = 1000.0 / 256.0;
   float       blend;
   spldat      *sd = EFdefog.sd;

   while (true)                                                                  //  loop all edit pixels
   {
      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) break;
      if (blend == 0) continue;

      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      R1 = pix1[0];                                                              //  input RGB values
      G1 = pix1[1];
      B1 = pix1[2];

      R3 = pix3[0];                                                              //  current output image RGB
      G3 = pix3[1];
      B3 = pix3[2];

      //  get fog/haze curve value = F(pixel brightness)

      pixB1 = 0.333 * (R1 + G1 + B1);                                            //  pixel brightness, 0 to 255.9
      ii = coeff * pixB1;                                                        //  curve index 0-999
      if (ii < 0) ii = 0;
      if (ii > 999) ii = 999;
      defog = sd->yval[0][ii];                                                   //  0 ... 1

      R9 = R1 - 100 * defog;                                                     //  remove equal amounts RGB
      G9 = G1 - 100 * defog;
      B9 = B1 - 100 * defog;

      B9 = B9 - 200 * blue * defog;                                              //  remove additional blue

      RGBfix(R9,G9,B9);                                                          //  clamp 0-255

      pixB2 = 0.333 * (R9 + G9 + B9);                                            //  new pixel brightness
      R = pixB2 / (pixB1 + 1);                                                   //  new / old brightness ratio
      R = R + 10 * bright * defog;                                               //  restore lost brightness

      R9 = R * R9;                                                               //  new pixel RGB
      G9 = R * G9;
      B9 = R * B9;

      RGBfix(R9,G9,B9);                                                          //  clamp 0-255

      if (Fpaintedits)                                                           //  gradual edit within mouse circle
      {
         if (blend > 0)
         {
            R3 = blend * R9 + (1-blend) * R3;                                    //  increase edit
            G3 = blend * G9 + (1-blend) * G3;
            B3 = blend * B9 + (1-blend) * B3;
         }

         else if (blend < 0)
         {
            R3 = -blend * R1 + (1+blend) * R3;                                   //  decrease edit
            G3 = -blend * G1 + (1+blend) * G3;
            B3 = -blend * B1 + (1+blend) * B3;
         }
      }

      else                                                                       //  full edit for image or area
      {
         R3 = blend * R9 + (1-blend) * R1;                                       //  0 - full edit for blend 0 - max.
         G3 = blend * G9 + (1-blend) * G1;
         B3 = blend * B9 + (1-blend) * B1;
      }

      pix3[0] = R3;                                                              //  set output pixel
      pix3[1] = G3;
      pix3[2] = B3;
   }

   return 0;
}


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

//  red eye removal function

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

   int      Nredmem = 0, maxredmem = 100;

   editfunc    EFredeye;

   #define PIXRED(pix) (25*pix[0]/(PIXBRIGHT(pix)+1))                            //  red brightness 0-100%
}


//  menu function

void m_redeyes(GtkWidget *, ch *menu)
{
   using namespace redeye_names;

   int      redeye_dialog_event(zdialog *zd, ch *event);
   void     redeye_mousefunc();

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

   F1_help_topic = "red eyes";

   Plog(1,"m_redeyes \n");

   EFredeye.menufunc = m_redeyes;
   EFredeye.menuname = "Red Eyes";
   EFredeye.Farea = 1;                                                           //  select area ignored
   EFredeye.mousefunc = redeye_mousefunc;

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

   zdialog *zd = zdialog_new("Red Eye Reduction",Mwin,"OK"," X ",null);
   EFredeye.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",redeye_message);
   zdialog_run(zd,redeye_dialog_event,"save");                                   //  run dialog - parallel

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


//  dialog event and completion callback function

int redeye_dialog_event(zdialog *zd, ch *event)
{
   using namespace redeye_names;

   void     redeye_mousefunc();

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

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

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

   return 1;
}


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()
{
   using namespace redeye_names;

   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
   {
      px = Mxclick;                                                              //  click position
      py = Myclick;
      if (px < 0 || px > Eww-1 || py < 0 || py > Ehh-1)                          //  outside image area
         return;

      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
      Fpaint2();
   }

   if (RMclick)                                                                  //  right mouse click
   {
      px = Mxclick;                                                              //  click position
      py = Myclick;
      ii = redeye_find(px,py);                                                   //  find red-eye
      if (ii >= 0) redeye_remove(ii);                                            //  if found, remove
      Fpaint2();
   }

   LMclick = RMclick = 0;

   if (Mxdrag || Mydrag)                                                         //  mouse drag underway
   {
      px = Mxdown;                                                               //  initial position
      py = Mydown;
      ww = Mxdrag - Mxdown;                                                      //  increment
      hh = Mydrag - Mydown;
      Mxdrag = Mydrag = 0;
      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 > Eww-1) ww = Eww-1 - px;
      if (py + hh > Ehh-1) hh = Ehh-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
   }

   return;
}


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

int redeye_createF(int cx, int cy)
{
   using namespace redeye_names;

   int         cx0, cy0, cx1, cy1, px, py, rad, radlim;
   int         loops, ii;
   int         Tnpix, Rnpix, R2npix;
   float       rd, rcx, rcy, redpart;
   float       Tsum, Rsum, R2sum, Tavg, Ravg, R2avg;
   float       sumx, sumy, sumr;
   float       *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(E3pxm,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(E3pxm,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 (fabsf(cx0 - rcx) > 0.6 * rad) break;                                   //  give up if big movement
      if (fabsf(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)
{
   using namespace redeye_names;

   int      rad, radlim;

   draw_mousearc(cx,cy,2*ww,2*hh,0,0);                                           //  draw ellipse around mouse pointer

   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)
{
   using namespace redeye_names;

   int         cx, cy, ww, hh, px, py, rad, clicks;
   float       rd, thresh, tstep;
   ch          type;
   float       *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 threshold
      tstep = redmem[ii].tstep;                                                  //  redness step size
      draw_mousearc(0,0,0,0,1,0);                                                //  erase mouse ellipse
   }

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

   for (py = cy-rad; py <= cy+rad; py++)                                         //  darken pixels over threshold
   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(E3pxm,px,py);                                                //  set redness = threshold
      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 threshold and step size for stepwise darkening.

void redeye_distr(int ii)
{
   using namespace redeye_names;

   int         cx, cy, ww, hh, rad, px, py;
   int         bin, npix, dbins[20], bsum, blim;
   float       rd, maxred, minred, redpart, dbase, dstep;
   ch          type;
   float       *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(E3pxm,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(E3pxm,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 threshold
   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)
{
   using namespace redeye_names;

   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)
{
   using namespace redeye_names;

   int      cx, cy, rad, px, py;
   float    *pix1, *pix3;
   int      nc = E1pxm->nc, pcc = nc * sizeof(float);

   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(E1pxm,px,py);
      pix3 = PXMpix(E3pxm,px,py);
      memcpy(pix3,pix1,pcc);
   }

   for (ii++; ii < Nredmem; ii++)
      redmem[ii-1] = redmem[ii];
   Nredmem--;

   draw_mousearc(0,0,0,0,1,0);                                                   //  erase mouse ellipse
   return;
}


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

int redeye_radlim(int cx, int cy)
{
   using namespace redeye_names;

   int radlim = 100;
   if (cx < 100) radlim = cx;
   if (Eww-1 - cx < 100) radlim = Eww-1 - cx;
   if (cy < 100) radlim = cy;
   if (Ehh-1 - cy < 100) radlim = Ehh-1 - cy;
   return radlim;
}


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

//  Smart Erase menu function
//  Replace pixels inside a mouse-selected area with pixels outside the area.

namespace smarterase_names
{
   editfunc    EFsmarterase;
}


//  menu function

void m_smart_erase(GtkWidget *, ch *menu)
{
   using namespace smarterase_names;

   int smart_erase_dialog_event(zdialog* zd, ch *event);

   ch       *erase_message = "Drag mouse to select. Erase. Repeat. \n"
                             "Click: extend selection to mouse.";
   F1_help_topic = "smart erase";

   Plog(1,"m_smart_erase \n");

   EFsmarterase.menufunc = m_smart_erase;
   EFsmarterase.menuname = "Smart Erase";
   EFsmarterase.Farea = 0;                                                       //  select area deleted
   EFsmarterase.mousefunc = sa_mouse_select;                                     //  mouse function (uses select area method)

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

/***
       _________________________________________
      |           Smart Erase                   |
      |                                         |
      | Drag mouse to select. Erase. Repeat.    |
      | Click: extend selection to mouse.       |
      |                                         |
      | Radius [ 10 ]    Blur [ 1.5 ]           |
      | [New Area] [Show] [Hide] [Erase] [Undo] |
      |                                         |
      |                                [OK] [X] |
      |_________________________________________|

***/

   zdialog *zd = zdialog_new("Smart Erase",Mwin,"OK"," X ",null);
   EFsmarterase.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",erase_message,"space=3");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labr","hb2","Radius","space=5");
   zdialog_add_widget(zd,"zspin","radius","hb2","1|30|1|10");
   zdialog_add_widget(zd,"label","labb","hb2","Blur","space=10");
   zdialog_add_widget(zd,"zspin","blur","hb2","0|9|0.5|1");
   zdialog_add_widget(zd,"hbox","hb3","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","newarea","hb3","New Area","space=3");
   zdialog_add_widget(zd,"button","show","hb3","Show","space=3");
   zdialog_add_widget(zd,"button","hide","hb3","Hide","space=3");
   zdialog_add_widget(zd,"button","erase","hb3","Erase","space=3");
   zdialog_add_widget(zd,"button","undo1","hb3","Undo","space=3");

   sa_clear();                                                                   //  clear area if any
   sa_pixmap_create();                                                           //  allocate select area pixel maps
   sa_mode = sa_mode_mouse;                                                      //  mode = select by mouse
   sa_stat = sa_stat_edit;                                                       //  status = active edit
   sa_fww = E1pxm->ww;
   sa_fhh = E1pxm->hh;
   sa_searchrange = 1;                                                           //  search within mouse radius
   sa_mouseradius = 10;                                                          //  initial mouse select radius
   sa_lastx = sa_lasty = 0;                                                      //  initz. for sa_mouse_select
   takeMouse(sa_mouse_select,0);                                                 //  use select area mouse function
   sa_show(1,0);

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


//  dialog event and completion function

int smart_erase_dialog_event(zdialog *zd, ch *event)
{
   using namespace smarterase_names;

   void smart_erase_func(int mode);
   int  smart_erase_blur(float radius);

   float       radius;

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

   if (zd->zstat)
   {
      sa_clear();                                                                //  clear select area
      freeMouse();                                                               //  disconnect mouse
      if (zd->zstat == 1) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   if (strmatch(event,"radius"))
      zdialog_fetch(zd,"radius",sa_mouseradius);

   if (strmatch(event,"newarea")) {
      sa_clear();
      sa_lastx = sa_lasty = 0;                                                   //  forget last click
      sa_pixmap_create();                                                        //  allocate select area pixel maps
      sa_mode = sa_mode_mouse;                                                   //  mode = select by mouse
      sa_stat = sa_stat_edit;                                                    //  status = active edit
      sa_fww = E1pxm->ww;
      sa_fhh = E1pxm->hh;
      sa_show(1,0);
      takeMouse(sa_mouse_select,0);
   }

   if (strmatch(event,"show")) {
      sa_show(1,0);
      takeMouse(sa_mouse_select,0);
   }

   if (strmatch(event,"hide")) {
      sa_show(0,0);
      freeMouse();
   }

   if (strmatch(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,0);
   }

   if (strmatch(event,"undo1"))                                                  //  dialog undo, undo last erase
      smart_erase_func(2);

   return 1;
}


//  erase the area or restore last erased area
//  mode = 1 = erase, mode = 2 = restore

void smart_erase_func(int mode)
{
   using namespace smarterase_names;

   int         px, py, npx, npy;
   int         qx, qy, sx, sy, tx, ty;
   int         ww, hh, ii, rad, inc, cc;
   int         dist2, mindist2;
   float       slope;
   ch          *pmap;
   float       *pix1, *pix3;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   if (sa_stat != sa_stat_fini) return;                                          //  nothing selected
   if (! sa_validate()) return;                                                  //  area invalid for curr. image file

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

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

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

   Fpaint2();                                                                    //  update window

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

   cc = ww * hh;                                                                 //  allocate pixel done map
   pmap = (ch *) zmalloc(cc,"smart erase");

   for (py = sa_miny; py < sa_maxy; py++)                                        //  loop all pixels in area
   for (px = sa_minx; px < sa_maxx; px++)
   {
      ii = py * ww + px;
      if (! sa_pixmap[ii]) 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
      {
         for (qx = px-rad; qx <= px+rad; qx++)                                   //  search within rad
         for (qy = py-rad; qy <= py+rad; qy++)
         {
            if (qx < 0 || qx >= ww) continue;                                    //  off image edge
            if (qy < 0 || qy >= hh) continue;
            ii = qy * ww + 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;                                       //  found edge, can quit now
      }

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

      qx = npx;                                                                  //  nearest edge pixel from px/py
      qy = npy;

      if (abs(qy - py) > abs(qx - px))                                           //  line px/py to qx/qy is more
      {                                                                          //    vertical than horizontal
         slope = 1.0 * (qx - px) / (qy - py);
         if (qy > py) inc = 1;
         else inc = -1;

         for (sy = py; sy != qy; sy += inc) {                                    //  sx/sy = line from px/py to qx/qy
            sx = px + slope * (sy - py);

            ii = sy * ww + sx;
            if (pmap[ii]) continue;                                              //  skip done pixels
            pmap[ii] = 1;

            tx = qx + (qx - sx);                                                 //  tx/ty = extended line from qx/qy
            ty = qy + (qy - sy);

            if (tx < 0) tx = 0;                                                  //  don't go off edge
            if (tx > ww-1) tx = ww-1;
            if (ty < 0) ty = 0;
            if (ty > hh-1) ty = hh-1;

            pix1 = PXMpix(E1pxm,tx,ty);                                          //  copy pixel from tx/ty to sx/sy
            pix3 = PXMpix(E3pxm,sx,sy);                                          //  simplified
            memcpy(pix3,pix1,pcc);
         }
      }

      else                                                                       //  more horizontal than vertical
      {
         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 * ww + sx;
            if (pmap[ii]) continue;
            pmap[ii] = 1;

            tx = qx + (qx - sx);
            ty = qy + (qy - sy);

            if (tx < 0) tx = 0;
            if (tx > ww-1) tx = ww-1;
            if (ty < 0) ty = 0;
            if (ty > hh-1) ty = hh-1;

            pix1 = PXMpix(E1pxm,tx,ty);
            pix3 = PXMpix(E3pxm,sx,sy);
            memcpy(pix3,pix1,pcc);
         }
      }
   }

   zfree(pmap);                                                                  //  free memory
   CEF->Fmods++;
   CEF->Fsaved = 0;

   Fpaint2();                                                                    //  update window
   return;
}


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

int smart_erase_blur(float radius)
{
   using namespace smarterase_names;

   int         ii, px, py, dx, dy, adx, ady;
   float       blur_weight[12][12];                                              //  up to blur radius = 10
   float       rad, radflat2;
   float       m, d, w, sum, weight;
   float       red, green, blue;
   float       *pix9, *pix3, *pixN;
   int         nc = E3pxm->nc;

   if (sa_stat != sa_stat_fini) return 0;

   rad = radius - 0.2;
   radflat2 = rad * rad;

   for (dx = 0; dx < 12; dx++)                                                   //  clear weights array
   for (dy = 0; dy < 12; 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(radflat2 + radflat2);                                                //  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;

   E9pxm = PXM_copy(E3pxm);                                                      //  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 * E1pxm->ww + px;
      if (! sa_pixmap[ii]) continue;                                             //  pixel not in area

      pix9 = PXMpix(E9pxm,px,py);                                                //  source pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  target pixel

      rad = radius;
      red = green = blue = 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 >= Eww) continue;                                //  omit pixels off edge
         if (py+dy < 0 || py+dy >= Ehh) continue;
         adx = abs(dx);
         ady = abs(dy);
         pixN = pix9 + (dy * Eww + dx) * nc;
         weight = blur_weight[adx][ady];                                         //  weight at distance (dx,dy)
         red += pixN[0] * weight;                                                //  accumulate contributions
         green += pixN[1] * weight;
         blue += pixN[2] * weight;
      }

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

   PXM_free(E9pxm);

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

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


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

//  remove halo function
//  drag mouse, slowly darken/brighten the brightest/darkest pixels within mouse circle

namespace remove_halo_names
{
   int      mode = 1;                                                            //  1/2 = reduce halo / undo
   int      Fbd;                                                                 //  1/0 = bright / dark halo
   float    Mrad;                                                                //  mouse radius
   float    Power;                                                               //  power at center, edge

   editfunc    EFremovehalo;
}


//  menu function

void m_remove_halo(GtkWidget *, ch *menu)
{
   using namespace remove_halo_names;

   int   remove_halo_dialog_event(zdialog* zd, ch *event);
   void  remove_halo_mousefunc();

   ch       *mess1 = " left drag: reduce halo   right drag: undo ";

   F1_help_topic = "remove halo";

   Plog(1,"m_remove_halo \n");

   EFremovehalo.menufunc = m_remove_halo;
   EFremovehalo.menuname = "Remove Halo";
   EFremovehalo.Farea = 1;                                                       //  select area ignored
   EFremovehalo.mousefunc = remove_halo_mousefunc;                               //  mouse function

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

   /***
             ____________________________________________
            |              Remove Halo                   |
            |                                            |
            | left drag: reduce halo  right drag: undo   |
            |                                            |
            | mouse radius [___]   power [___]           |
            | (o) bright halo  (o) dark halo             |                       //  24.20
            |                                            |
            |                                   [OK] [X] |
            |____________________________________________|

   ***/

   zdialog *zd = zdialog_new("Remove Halo",Mwin,"OK"," X ",null);
   EFremovehalo.zd = zd;

   zdialog_add_widget(zd,"label","labm","dialog",mess1,"space=5");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labbr","hb2","mouse radius","space=3");
   zdialog_add_widget(zd,"zspin","Mrad","hb2","1|100|1|10");
   zdialog_add_widget(zd,"label","space","hb2","","space=8");
   zdialog_add_widget(zd,"label","labsc","hb2","power","space=3");
   zdialog_add_widget(zd,"zspin","Power","hb2","0|100|1|100");
   zdialog_add_widget(zd,"hbox","hb3","dialog",0,"space=2");
   zdialog_add_widget(zd,"radio","bright","hb3","bright halo","space=3");
   zdialog_add_widget(zd,"radio","dark","hb3","dark halo","space=8");

   zdialog_run(zd,remove_halo_dialog_event,"save");                              //  run dialog, parallel
   zdialog_send_event(zd,"Mrad");                                                //  initialize mouse params

   zdialog_stuff(zd,"bright",1);                                                 //  initz. bright halo mode
   Fbd = 1;

   mode = 1;                                                                     //  initz. paint mode

   takeMouse(remove_halo_mousefunc,drawcursor);                                  //  connect mouse function
   return;
}


//  dialog event and completion callback function

int remove_halo_dialog_event(zdialog *zd, ch *event)
{
   using namespace remove_halo_names;

   void  remove_halo_mousefunc();

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

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

   if (strmatch(event,"focus"))                                                  //  toggle mouse capture
      takeMouse(remove_halo_mousefunc,drawcursor);

   if (zstrstr("Mrad Power",event))                                              //  get new mouse attributes
   {
      zdialog_fetch(zd,"Mrad",Mrad);                                             //  mouse radius
      zdialog_fetch(zd,"Power",Power);                                           //  mouse power
      Power = 0.01 * Power;                                                      //  scale 0 - 1.0
   }
   
   if (zstrstr("bright dark",event))                                             //  get bright/dark mode = 1/0
      zdialog_fetch(zd,"bright",Fbd);

   return 1;
}


//  pixel paint mouse function

void remove_halo_mousefunc()
{
   using namespace remove_halo_names;

   void  remove_halo_bright(int px, int py);
   void  remove_halo_dark(int px, int py);

   static int  pmxdown = 0, pmydown = 0;
   int         px, py;

   if (LMclick || RMclick)
   {
      if (LMclick) mode = 1;                                                     //  left click, paint
      if (RMclick) mode = 2;                                                     //  right click, erase

      px = Mxclick;
      py = Myclick;
      if (Fbd) remove_halo_bright(px,py);                                        //  do 1 block of pixels
      else remove_halo_dark(px,py);
   }

   else if (Mxdrag || Mydrag)                                                    //  drag in progress
   {
      if (Mbutton == 1) mode = 1;                                                //  left drag, paint
      if (Mbutton == 3) mode = 2;                                                //  right drag, erase

      px = Mxdrag;
      py = Mydrag;

      if (Mxdown != pmxdown || Mydown != pmydown) {                              //  new drag
         pmxdown = Mxdown;
         pmydown = Mydown;
      }

      if (Fbd) remove_halo_bright(px,py);                                        //  do 1 block of pixels
      else remove_halo_dark(px,py);

      LMclick = RMclick = Mxdrag = Mydrag = 0;
      return;
   }

   LMclick = RMclick = Mxdrag = Mydrag = 0;
   draw_mousecircle(Mxposn,Myposn,Mrad,0,0);                                     //  draw mouse circle

   return;
}


//  process pixels within mouse radius

void remove_halo_bright(int px, int py)
{
   using namespace remove_halo_names;

   float       *pix1, *pix3;
   float       Drad, Pthresh, Rpower;
   float       B1, B3, BR;
   int         Bhist[768], Spix, Npix;
   int         dx, dy, qx, qy;
   int         ii, jj, rr;

   for (ii = 0; ii < 768; ii++)                                                  //  clear brightness histogram
      Bhist[ii] = 0;
   Spix = Npix = 0;

   if (Mrad > 16) jj = 2;                                                        //  if large radius, sample 1/4 pixels
   else jj = 1;

   for (dy = -Mrad; dy <= Mrad; dy += jj)                                        //  loop pixels around mouse
   for (dx = -Mrad; dx <= Mrad; dx += jj)
   {
      qx = px + dx;
      qy = py + dy;

      if (qx < 0 || qx > Eww-1) continue;
      if (qy < 0 || qy > Ehh-1) continue;

      pix3 = PXMpix(E3pxm,qx,qy);                                                //  accumulate brightness histogram
      ii = pix3[0] + pix3[1] + pix3[2];                                          //  RGB weighted equally
      if (ii > 767) ii = 767;                                                    //  should not happen
      Bhist[ii] += 1;
      Npix += 1;
   }

   for (ii = 767; ii > 0; ii--)                                                  //  find brightest pixels
   {
      Spix += Bhist[ii];
      if (Spix > Power * 0.05 * Npix) break;                                     //  at most 5%
   }

   Pthresh = ii;                                                                 //  darken pixels > Pthresh

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   for (dy = -Mrad; dy <= Mrad; dy++)                                            //  loop pixels around mouse
   for (dx = -Mrad; dx <= Mrad; dx++)
   {
      qx = px + dx;
      qy = py + dy;

      if (qx < 0 || qx > Eww-1) continue;
      if (qy < 0 || qy > Ehh-1) continue;

      Drad = sqrtf(dx*dx + dy*dy);                                               //  distance from mouse pointer
      if (Drad > Mrad) continue;                                                 //  > Mrad, ignore

      Rpower = Power * Drad / Mrad;                                              //  mouse center...edge >> 1...0

      pix1 = PXMpix(E1pxm,qx,qy);                                                //  original pixel
      pix3 = PXMpix(E3pxm,qx,qy);                                                //  edited pixel

      B1 = pix1[0] + pix1[1] + pix1[2];                                          //  pix1 brightness
      B3 = pix3[0] + pix3[1] + pix3[2];                                          //  pix3 brightness
      BR = (B3 + 1) / (B1 + 1);                                                  //  pix3/pix1 ratio, 0 - 1

      if (mode == 1)                                                             //  reduce halo
      {
         if (B3 < Pthresh) continue;                                             //  below Pthresh, ignore
         BR = BR - Rpower * 0.05;                                                //  reduce brightness up to 5%
         if (BR < 0) BR = 0;
         pix3[0] = pix1[0] * BR;
         pix3[1] = pix1[1] * BR;
         pix3[2] = pix1[2] * BR;
      }

      if (mode == 2)                                                             //  restore original image
      {
         BR = BR + Rpower * 0.1;
         if (BR > 1.0) BR = 1.0;
         pix3[0] = pix1[0] * BR;
         pix3[1] = pix1[1] * BR;
         pix3[2] = pix1[2] * BR;
      }
   }

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

   px = px - Mrad - 1;                                                           //  repaint modified area
   py = py - Mrad - 1;
   rr = 2 * Mrad + 3;
   Fpaint3(px,py,rr,rr,cr);

   draw_mousecircle(Mxposn,Myposn,Mrad,0,cr);                                    //  draw mouse circle

   draw_context_destroy(draw_context);
   return;
}


void remove_halo_dark(int px, int py)                                            //  24.20
{
   using namespace remove_halo_names;

   float       *pix1, *pix3;
   float       Drad, Pthresh, Rpower;
   float       B1, B3, BR;
   int         Bhist[768], Spix, Npix;
   int         dx, dy, qx, qy;
   int         ii, jj, rr;

   for (ii = 0; ii < 768; ii++)                                                  //  clear brightness histogram
      Bhist[ii] = 0;
   Spix = Npix = 0;

   if (Mrad > 16) jj = 2;                                                        //  if large radius, sample 1/4 pixels
   else jj = 1;

   for (dy = -Mrad; dy <= Mrad; dy += jj)                                        //  loop pixels around mouse
   for (dx = -Mrad; dx <= Mrad; dx += jj)
   {
      qx = px + dx;
      qy = py + dy;

      if (qx < 0 || qx > Eww-1) continue;
      if (qy < 0 || qy > Ehh-1) continue;

      pix3 = PXMpix(E3pxm,qx,qy);                                                //  accumulate brightness histogram
      ii = pix3[0] + pix3[1] + pix3[2];                                          //  RGB weighted equally
      if (ii > 767) ii = 767;                                                    //  should not happen
      Bhist[ii] += 1;
      Npix += 1;
   }

   for (ii = 0; ii < 768; ii++)                                                  //  find darkest pixels
   {
      Spix += Bhist[ii];
      if (Spix > Power * 0.05 * Npix) break;                                     //  at most 5%
   }

   Pthresh = ii;                                                                 //  brighten pixels < Pthresh

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   for (dy = -Mrad; dy <= Mrad; dy++)                                            //  loop pixels around mouse
   for (dx = -Mrad; dx <= Mrad; dx++)
   {
      qx = px + dx;
      qy = py + dy;

      if (qx < 0 || qx > Eww-1) continue;
      if (qy < 0 || qy > Ehh-1) continue;

      Drad = sqrtf(dx*dx + dy*dy);                                               //  distance from mouse pointer
      if (Drad > Mrad) continue;                                                 //  > Mrad, ignore

      Rpower = Power * Drad / Mrad;                                              //  mouse center...edge >> 1...0

      pix1 = PXMpix(E1pxm,qx,qy);                                                //  original pixel
      pix3 = PXMpix(E3pxm,qx,qy);                                                //  edited pixel

      B1 = pix1[0] + pix1[1] + pix1[2];                                          //  pix1 brightness
      B3 = pix3[0] + pix3[1] + pix3[2];                                          //  pix3 brightness
      BR = (B3 + 1) / (B1 + 1);                                                  //  pix3/pix1 ratio, 0 - 1

      if (mode == 1)                                                             //  reduce halo
      {
         if (B3 > Pthresh) continue;                                             //  above Pthresh, ignore
         BR = BR + Rpower * 0.1;                                                 //  increase brightness up to 10%
         pix3[0] = pix1[0] * BR;
         pix3[1] = pix1[1] * BR;
         pix3[2] = pix1[2] * BR;
      }

      if (mode == 2)                                                             //  restore original image
      {
         BR = BR + Rpower * 0.1;
         if (BR > 1.0) BR = 1.0;
         pix3[0] = pix1[0] * BR;
         pix3[1] = pix1[1] * BR;
         pix3[2] = pix1[2] * BR;
      }
   }

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

   px = px - Mrad - 1;                                                           //  repaint modified area
   py = py - Mrad - 1;
   rr = 2 * Mrad + 3;
   Fpaint3(px,py,rr,rr,cr);

   draw_mousecircle(Mxposn,Myposn,Mrad,0,cr);                                    //  draw mouse circle

   draw_context_destroy(draw_context);
   return;
}


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

//  Attenuate JPEG compression artifacts

namespace jpeg_artifacts_names
{
   editfunc    EFjpeg_artifacts;
   float       matchlimit;                                                       //  pixel block match limit
   float       conlimit;                                                         //  block blend contrast limit
   uint8       *pixmark;

   typedef struct {                                                              //  block of monotone pixels
      uint16   px, py;                                                           //  (image ww/hh limit = 64K)
      uint8    ww, hh; 
   }  pixblock;

   pixblock    *PB;
   int         NB;
}


//  menu function

void m_jpeg_artifacts(GtkWidget *, ch  *menu) 
{
   using namespace jpeg_artifacts_names;

   int jpeg_artifacts_dialog_event(zdialog *zd, ch *event);
   void * jpeg_artifacts_thread(void *);

   F1_help_topic = "jpeg artifacts";

   Plog(1,"m_jpeg_artifacts \n");

   EFjpeg_artifacts.menuname = "JPEG Artifacts";
   EFjpeg_artifacts.menufunc = m_jpeg_artifacts;
   EFjpeg_artifacts.Farea = 2;                                                   //  select area usable
   EFjpeg_artifacts.Fscript = 1;                                                 //  scripting supported
   EFjpeg_artifacts.threadfunc = jpeg_artifacts_thread;

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

   int   cc = Eww * Ehh;                                                         //  allocate pixmark memory
   pixmark = (uint8 *) zmalloc(cc,"jpeg_artifacts");
   PB = (pixblock *) zmalloc(cc * sizeof(pixblock),"jpeg_artifacts");
   
/***
          _____________________________
         |      JPEG Artifacts         |
         |                             |
         |     Match Limit [___]       |
         |  Contrast Limit [___]       |
         |                             |
         |    [Reset] [Apply] [OK] [X] |
         |_____________________________|

***/

   zdialog *zd = zdialog_new("JPEG Artifacts",Mwin,"Reset","Apply","OK"," X ",null);
   EFjpeg_artifacts.zd = zd;
   
   zdialog_add_widget(zd,"hbox","hbm","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labm","hbm","Match Limit","space=5");
   zdialog_add_widget(zd,"zspin","matchlimit","hbm","0.0|0.99|0.01|0.9");
   
   zdialog_add_widget(zd,"hbox","hbc","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labc","hbc","Contrast Limit","space=5");
   zdialog_add_widget(zd,"zspin","conlimit","hbc","0.0|0.99|0.01|0.8");
   
   matchlimit = 0.9;                                                             //  defaults
   conlimit = 0.8;

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


//  dialog event and completion function

int jpeg_artifacts_dialog_event(zdialog *zd, ch *event)
{
   using namespace jpeg_artifacts_names;
   
   if (strmatch(event,"done")) zd->zstat = 3;                                    //  apply and quit
   if (strmatch(event,"cancel")) zd->zstat = -1;                                 //  cancel
   if (strmatch(event,"apply")) zd->zstat = 2;                                   //  from script

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  [reset]
         zd->zstat = 0;                                                          //  keep dialog alive
         edit_reset();
         return 1;
      }

      else if (zd->zstat == 2) {                                                 //  [apply]
         zd->zstat = 0;                                                          //  keep dialog alive
         thread_signal();                                                        //  start thread
         return 1;
      }

      else if (zd->zstat == 3) edit_done(0);                                     //  [OK]

      else edit_cancel(0);                                                       //  discard edit

      zfree(pixmark);                                                            //  free memory
      zfree(PB);
      return 1;
   }
   
   if (strmatch(event,"matchlimit"))
      zdialog_fetch(zd,"matchlimit",conlimit);

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

   return 1;
}


//  thread function

void * jpeg_artifacts_thread(void *)
{
   using namespace jpeg_artifacts_names;

   int         ii, dist;
   int         px, py, qx, qy, ww, hh;
   int         maxrow, area, maxarea, maxqx, maxqy;
   float       *pixp, *pixq;

   E9pxm = PXM_copy(E3pxm);                                                      //  this allows multiple [apply] 

   for (py = 8; py < Ehh-8; py++)                                                //  unmark all pixels
   for (px = 8; px < Eww-8; px++)
   {
      ii = py * Eww + px;
      pixmark[ii] = 0;
   }

   NB = 0;                                                                       //  block count
      
   for (py = 8; py < Ehh-8; py++)                                                //  loop all pixels
   for (px = 8; px < Eww-8; px++)
   {
      ii = py * Eww + px;                                                        //  skip marked pixels
      if (pixmark[ii]) continue;

      if (sa_stat == sa_stat_fini) {                                             //  select area active
         dist = sa_pixmap[ii];                                                   //  distance from area edge
         if (! dist) continue;                                                   //  outside area, skip
      }

      maxrow = 8;
      maxarea = 0;

      for (qy = py; qy < py+8; qy++)                                             //  loop rows py .. py+7
      {
         pixp = PXMpix(E1pxm,px,py);

         for (qx = px; qx < px+maxrow; qx++)                                     //  loop cols px .. px+maxrow-1
         {
            pixq = PXMpix(E1pxm,qx,qy);                                          //  count matching pixels in row
            if (PIXmatch(pixp,pixq) < matchlimit) break;
         }

         if (qx == px) break;                                                    //  end of matching rows

         qx--;                                                                   //  last matching col in row
         if (qx-px+1 < maxrow) maxrow = qx-px+1;                                 //  update maxrow if row shorter
         area = (qx-px+1) * (qy-py+1);                                           //  area (px,py) .. (qx,qy)
         if (area > maxarea) {
            maxarea = area;                                                      //  remember max area found
            maxqx = qx;                                                          //    anchored at (px,py)
            maxqy = qy;
         }
      }
      
      if (! maxarea) continue;
      
      ii = NB++;                                                                 //  save monotone block
      PB[ii].px = px;                                                            //  origin = px, py
      PB[ii].py = py;
      ww = maxqx-px+1;
      hh = maxqy-py+1;
      PB[ii].ww = ww;                                                            //  size = ww, hh
      PB[ii].hh = hh;
      
      for (qy = py; qy < py + hh; qy++)                                          //  mark pixels in block
      for (qx = px; qx < px + ww; qx++) 
      {
         ii = qy * Eww + qx;
         pixmark[ii] = 1;
      }
   }

   float    d1, d2, d3, d4;
   float    f1, f2, f3, f4;
   float    R, G, B;
   float    *pix1, *pix2, *pix3, *pix4;
   
   for (ii = 0; ii < NB; ii++)                                                   //  loop all blocks
   {
      px = PB[ii].px;
      py = PB[ii].py;
      ww = PB[ii].ww;
      hh = PB[ii].hh;

      for (qy = py; qy < py+hh; qy++)                                            //  loop pixels in block
      for (qx = px; qx < px+ww; qx++)
      {
         d1 = qy - py + 1;                                                       //  qx/qy distance from block edges
         d2 = py + hh - qy;
         d3 = qx - px + 1;
         d4 = px + ww - qx;

         f1 = d1 / (ww + hh + 2);                                                //  bilinear interpolation weights
         f2 = d2 / (ww + hh + 2);
         f3 = d3 / (ww + hh + 2);
         f4 = d4 / (ww + hh + 2);

         pix1 = PXMpix(E9pxm,qx,py+hh);                                          //  neighbor pixels for interpolation
         pix2 = PXMpix(E9pxm,qx,py-1);
         pix3 = PXMpix(E9pxm,px+ww,qy);
         pix4 = PXMpix(E9pxm,px-1,qy);

         pixq = PXMpix(E9pxm,qx,qy);
         if (PIXmatch(pix1,pixq) < conlimit) pix1 = pixq;                        //  replace high-contrast neighbors
         if (PIXmatch(pix2,pixq) < conlimit) pix2 = pixq;
         if (PIXmatch(pix3,pixq) < conlimit) pix3 = pixq;
         if (PIXmatch(pix4,pixq) < conlimit) pix4 = pixq;
         
         R = f1 * pix1[0] + f2 * pix2[0] + f3 * pix3[0] + f4 * pix4[0];          //  new RGB from neighbors
         G = f1 * pix1[1] + f2 * pix2[1] + f3 * pix3[1] + f4 * pix4[1];
         B = f1 * pix1[2] + f2 * pix2[2] + f3 * pix3[2] + f4 * pix4[2];

         pixq = PXMpix(E3pxm,qx,qy);                                             //  output pixel
         pixq[0] = R;
         pixq[1] = G;
         pixq[2] = B;
      }

      float *pix3;
      if (drandz() < 0.0) {
         pix3 = PXMpix(E3pxm,px,py);                                             //  mark block (debug tool) 
         pix3[0] = pix3[1] = pix3[2] = 0;
         pix3 = PXMpix(E3pxm,px+ww-1,py+hh-1);
         pix3[0] = pix3[1] = pix3[2] = 255;
      }
   }
   
   PXM_free(E9pxm);
   E9pxm = 0;
   
   CEF->Fmods++;                                                                 //  E3pxm modified
   CEF->Fsaved = 0;

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


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

   anti-alias: smooth the jaggies on high-contrast edges
   scale3x algorithm, modified to use 'closeness' in place of 'equal'

      3x3 pixel input block   3x3 pixel output block 
         A B C                   1 2 3
         D E F                   4 5 6
         G H I                   7 8 9

      input block = 3x3 pixels centered on input pixel E
      1=E; 2=E; 3=E; 4=E; 5=E; 6=E; 7=E; 8=E; 9=E;
      IF D==B AND D!=H AND B!=F then 1=D
      IF D==B AND D!=H AND B!=F AND E!=C then 2=B
      IF B==F AND B!=D AND F!=H AND E!=A then 2=B
      IF B==F AND B!=D AND F!=H then 3=F
      IF H==D AND H!=F AND D!=B AND E!=A then 4=D
      IF D==B AND D!=H AND B!=F AND E!=G then 4=D
      5=E
      IF B==F AND B!=D AND F!=H AND E!=I then 6=F
      IF F==H AND F!=B AND H!=D AND E!=C then 6=F
      IF H==D AND H!=F AND D!=B then 7=D
      IF F==H AND F!=B AND H!=D AND E!=G then 8=H
      IF H==D AND H!=F AND D!=B AND E!=I then 8=H
      IF F==H AND F!=B AND H!=D then 9=F
      output pixel E = mean of 3x3 output block

   Note: can be applied more than once, but 'painting' causes
   rapid deterioration, therefore this is not supported.

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


namespace anti_alias_names
{
   editfunc    EFanti_alias;
   int         applies;
   float       thresh;
}


void m_anti_alias(GtkWidget *, ch *menu) 
{
   using namespace anti_alias_names;

   int   anti_alias_dialog_event(zdialog *zd, ch *event);
   void  * anti_alias_thread(void *);

   F1_help_topic = "anti-alias";

   Plog(1,"m_anti_alias \n");

   EFanti_alias.menuname = "Anti-Alias";
   EFanti_alias.menufunc = m_anti_alias;
   EFanti_alias.Farea = 2;                                                       //  select area usable
   EFanti_alias.threadfunc = anti_alias_thread;                                  //  thread function
   EFanti_alias.Fscript = 1;                                                     //  allow script 

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

/***
          ___________________________________
         |          Anti-Alias               |
         |                                   |
         |  Threshold [___]                  |
         |                                   |
         |          [Reset] [Apply] [OK] [X] |
         |___________________________________|

***/

   zdialog *zd = zdialog_new("Anti-Alias",Mwin,"Reset","Apply","OK"," X ",null);
   zdialog_add_widget(zd,"hbox","hbthresh","dialog",0,"space=10");
   zdialog_add_widget(zd,"label","labthresh","hbthresh","Threshold","space=10");
   zdialog_add_widget(zd,"zspin","thresh","hbthresh","0.00|0.30|0.01|0.00");
   EFanti_alias.zd = zd;

   applies = 0;
   thresh = 0.0;

   zdialog_run(zd,anti_alias_dialog_event,"save");                               //  run dialog

   return;
}


//  dialog event and completion callback function

int anti_alias_dialog_event(zdialog * zd, ch *event)
{
   using namespace anti_alias_names;

   if (strmatch(event,"done")) zd->zstat = 3;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 4;                                  //  from f_open()
   
   if (strmatch(event,"apply")) {                                                //  from script                           24.30
      thread_signal();
      applies = 1;
      zd->zstat = 3;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  [reset]
      {
         zd->zstat = 0;                                                          //  keep dialog active
         edit_reset();
      }

      else if (zd->zstat == 2)                                                   //  [apply]
      {
         zd->zstat = 0;                                                          //  keep dialog active
         thread_signal();                                                        //  trigger thread
         applies++;
      }

      else if (zd->zstat == 3) {                                                 //  [OK]
         edit_addhist("applies: %d",applies);
         edit_done(0);
      }

      else edit_cancel(0);                                                       //  cancel
      
      return 1;
   }
   
   if (strmatch(event,"thresh"))                                                 //  get threshold value 0.0 - 1.0
      zdialog_fetch(zd,"thresh",thresh);

   return 1;
}


//  thread function

void * anti_alias_thread(void *)
{
   using namespace anti_alias_names;

   void * anti_alias_wthread(void *);

   E9pxm = PXM_copy(E3pxm);                                                      //  this facilitates multiple [apply] 

   do_wthreads(anti_alias_wthread,NSMP);                                         //  worker threads

   PXM_free(E9pxm);
   E9pxm = 0;

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

   Fpaint2();

   return 0;
}


//  worker thread

void * anti_alias_wthread(void *arg)
{
   using namespace anti_alias_names;

   int      index = *((int *) arg);
   int      ii, px, py, dist;
   int      pcc = 3 * sizeof(float);
   float    *pixA, *pixB, *pixC, *pixD, *pixE, *pixF, *pixG, *pixH, *pixI;
   float    pix1[3], pix2[3], pix3[3], pix4[3], pix5[3], pix6[3], pix7[3], pix8[3], pix9[3];
   float    R, G, B;
   
   for (py = index+1; py < Ehh-1; py += NSMP)                                    //  loop all pixels
   for (px = 1; px < Eww-1; px++)
   {
      if (sa_stat == sa_stat_fini) {                                             //  select area active
         ii = py * Eww + px;
         dist = sa_pixmap[ii];                                                   //  distance from area edge
         if (! dist) continue;                                                   //  outside area, skip
      }

      pixA = PXMpix(E9pxm,px-1,py-1);                                            //  input 3x3 block
      pixB = PXMpix(E9pxm,px,  py-1);                                            //    centered on px,py
      pixC = PXMpix(E9pxm,px+1,py-1);
      pixD = PXMpix(E9pxm,px-1,py  );
      pixE = PXMpix(E9pxm,px,  py  );                                            //  pixE is central pixel
      pixF = PXMpix(E9pxm,px+1,py  );
      pixG = PXMpix(E9pxm,px-1,py+1);
      pixH = PXMpix(E9pxm,px,  py+1);
      pixI = PXMpix(E9pxm,px+1,py+1);

      memcpy(pix1,pixE,pcc);                                                     //  output 3x3 block
      memcpy(pix2,pixE,pcc);                                                     //  initz. = pixE
      memcpy(pix3,pixE,pcc);
      memcpy(pix4,pixE,pcc);
      memcpy(pix5,pixE,pcc);
      memcpy(pix6,pixE,pcc);
      memcpy(pix7,pixE,pcc);
      memcpy(pix8,pixE,pcc);
      memcpy(pix9,pixE,pcc);

      if (PIXmatch(pixD,pixB) > PIXmatch(pixD,pixH) + thresh &&
          PIXmatch(pixD,pixB) > PIXmatch(pixB,pixF) + thresh) 
            memcpy(pix1,pixD,pcc);
            
      if (PIXmatch(pixD,pixB) > PIXmatch(pixD,pixH) + thresh &&
          PIXmatch(pixD,pixB) > PIXmatch(pixB,pixF) + thresh &&
          PIXmatch(pixD,pixB) > PIXmatch(pixE,pixC) + thresh) 
            memcpy(pix2,pixB,pcc);
            
      if (PIXmatch(pixB,pixF) > PIXmatch(pixB,pixD) + thresh &&
          PIXmatch(pixB,pixF) > PIXmatch(pixF,pixH) + thresh &&
          PIXmatch(pixB,pixF) > PIXmatch(pixE,pixA) + thresh) 
            memcpy(pix2,pixB,pcc);
            
      if (PIXmatch(pixB,pixF) > PIXmatch(pixB,pixD) + thresh &&
          PIXmatch(pixB,pixF) > PIXmatch(pixF,pixH) + thresh) 
            memcpy(pix3,pixF,pcc);
            
      if (PIXmatch(pixH,pixD) > PIXmatch(pixH,pixF) + thresh &&
          PIXmatch(pixH,pixD) > PIXmatch(pixD,pixB) + thresh &&
          PIXmatch(pixH,pixD) > PIXmatch(pixE,pixA) + thresh) 
            memcpy(pix4,pixD,pcc);
            
      if (PIXmatch(pixD,pixB) > PIXmatch(pixD,pixH) + thresh &&
          PIXmatch(pixD,pixB) > PIXmatch(pixB,pixF) + thresh &&
          PIXmatch(pixD,pixB) > PIXmatch(pixE,pixG) + thresh) 
            memcpy(pix4,pixD,pcc);
            
      if (PIXmatch(pixB,pixF) > PIXmatch(pixB,pixD) + thresh &&
          PIXmatch(pixB,pixF) > PIXmatch(pixF,pixH) + thresh &&
          PIXmatch(pixB,pixF) > PIXmatch(pixE,pixI) + thresh) 
            memcpy(pix6,pixF,pcc);
            
      if (PIXmatch(pixF,pixH) > PIXmatch(pixF,pixB) + thresh &&
          PIXmatch(pixF,pixH) > PIXmatch(pixH,pixD) + thresh &&
          PIXmatch(pixF,pixH) > PIXmatch(pixE,pixC) + thresh) 
            memcpy(pix6,pixF,pcc);
            
      if (PIXmatch(pixH,pixD) > PIXmatch(pixH,pixF) + thresh &&
          PIXmatch(pixH,pixD) > PIXmatch(pixD,pixB) + thresh) 
            memcpy(pix7,pixD,pcc);
            
      if (PIXmatch(pixF,pixH) > PIXmatch(pixF,pixB) + thresh &&
          PIXmatch(pixF,pixH) > PIXmatch(pixH,pixD) + thresh &&
          PIXmatch(pixF,pixH) > PIXmatch(pixE,pixG) + thresh) 
            memcpy(pix8,pixH,pcc);
            
      if (PIXmatch(pixH,pixD) > PIXmatch(pixH,pixF) + thresh &&
          PIXmatch(pixH,pixD) > PIXmatch(pixD,pixB) + thresh &&
          PIXmatch(pixH,pixD) > PIXmatch(pixE,pixI) + thresh) 
            memcpy(pix8,pixH,pcc);
            
      if (PIXmatch(pixF,pixH) > PIXmatch(pixF,pixB) + thresh &&
          PIXmatch(pixF,pixH) > PIXmatch(pixH,pixD) + thresh) 
            memcpy(pix9,pixF,pcc);
      
      R = pix1[0] + pix2[0] + pix3[0] + pix4[0] + pix5[0]                        //  get mean of pix1-pix9
        + pix6[0] + pix7[0] + pix8[0] + pix9[0];
      R = R * 0.1111;

      G = pix1[1] + pix2[1] + pix3[1] + pix4[1] + pix5[1] 
        + pix6[1] + pix7[1] + pix8[1] + pix9[1];
      G = G * 0.1111;

      B = pix1[2] + pix2[2] + pix3[2] + pix4[2] + pix5[2] 
        + pix6[2] + pix7[2] + pix8[2] + pix9[2];
      B = B * 0.1111;
      
      pixE = PXMpix(E3pxm,px,py);                                                //  output pixel

      pixE[0] = R;
      pixE[1] = G;
      pixE[2] = B;
   }

   return 0;
}


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

//  Adjust RGB menu function
//  Adjust Brightness, contrast, and color levels using RGB or CMY colors

namespace adjust_RGB_names
{
   editfunc    EF_RGB;                                                           //  edit function data
   float       inputs[8];
}


//  menu function

void m_adjust_RGB(GtkWidget *, ch *menu)
{
   using namespace adjust_RGB_names;

   int    RGB_dialog_event(zdialog *zd, ch *event);
   void * RGB_thread(void *);

   F1_help_topic = "adjust RGB";

   Plog(1,"m_adjust_RGB \n");

   EF_RGB.menuname = "Adjust RGB";
   EF_RGB.menufunc = m_adjust_RGB;
   EF_RGB.FprevReq = 1;                                                          //  use preview
   EF_RGB.Farea = 2;                                                             //  select area usable
   EF_RGB.Fscript = 1;                                                           //  scripting supported
   EF_RGB.Fpaintedits = 1;                                                       //  paint edits supported
   EF_RGB.threadfunc = RGB_thread;                                               //  thread function

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

/***
          ________________________________
         |                                |
         |   +Brightness    =====[]=====  |
         |    +Red -Cyan    =====[]=====  |
         | +Green -Magenta  =====[]=====  |
         |   +Blue -Yellow  =====[]=====  |
         |                                |
         |     Contrast     =====[]=====  |
         |       Red        =====[]=====  |
         |      Green       =====[]=====  |
         |       Blue       =====[]=====  |
         |                                |
         |               [Reset] [OK] [X] |
         |________________________________|

***/

   zdialog *zd = zdialog_new("Adjust RGB",Mwin,"Reset","OK"," X ",null);
   EF_RGB.zd = zd;

   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb2",0,"homog");
   zdialog_add_widget(zd,"vbox","vb2","hb2",0,"homog|expand");
   zdialog_add_widget(zd,"label","labBriteDens","vb1","+Brightness");
   zdialog_add_widget(zd,"label","labRedDens","vb1","+Red -Cyan");
   zdialog_add_widget(zd,"label","labGreenDens","vb1","+Green -Magenta");
   zdialog_add_widget(zd,"label","labBlueDens","vb1","+Blue -Yellow");
   zdialog_add_widget(zd,"hsep","sep1","vb1");
   zdialog_add_widget(zd,"label","labContrast","vb1","Contrast All");
   zdialog_add_widget(zd,"label","labRedCon","vb1","Contrast Red");
   zdialog_add_widget(zd,"label","labGreenCon","vb1","Contrast Green");
   zdialog_add_widget(zd,"label","labBlueCon","vb1","Contrast Blue");
   zdialog_add_widget(zd,"hscale2","BriteDens","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale2","RedDens","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale2","GreenDens","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale2","BlueDens","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hsep","sep2","vb2");
   zdialog_add_widget(zd,"hscale2","Contrast","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale2","RedCon","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale2","GreenCon","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale2","BlueCon","vb2","-1|+1|0.001|0","expand");
   
   zdialog_rescale(zd,"BriteDens",-1,0,+1);                                      //  expand scale around neutral value
   zdialog_rescale(zd,"RedDens",-1,0,+1);
   zdialog_rescale(zd,"GreenDens",-1,0,+1);
   zdialog_rescale(zd,"BlueDens",-1,0,+1);
   zdialog_rescale(zd,"Contrast",-1,0,+1);
   zdialog_rescale(zd,"RedCon",-1,0,+1);
   zdialog_rescale(zd,"GreenCon",-1,0,+1);
   zdialog_rescale(zd,"BlueCon",-1,0,+1);

   zdialog_resize(zd,300,0);
   zdialog_load_inputs(zd);                                                      //  restore prior inputs
   zdialog_run(zd,RGB_dialog_event,"save");                                      //  run dialog - parallel

   zdialog_send_event(zd,"apply");
   return;
}


//  RGB dialog event and completion function

int RGB_dialog_event(zdialog *zd, ch *event)                                     //  RGB dialog event function
{
   using namespace adjust_RGB_names;

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

   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();
      thread_signal();
      thread_wait();                                                             //  required for paint edits
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  reset
         zd->zstat = 0;                                                          //  keep dialog active
         zdialog_stuff(zd,"BriteDens",0);
         zdialog_stuff(zd,"RedDens",0);
         zdialog_stuff(zd,"GreenDens",0);
         zdialog_stuff(zd,"BlueDens",0);
         zdialog_stuff(zd,"Contrast",0);
         zdialog_stuff(zd,"RedCon",0);
         zdialog_stuff(zd,"GreenCon",0);
         zdialog_stuff(zd,"BlueCon",0);
         edit_reset();
         return 1;
      }

      if (zd->zstat == 2) {                                                      //  done
         edit_fullsize();                                                        //  get full size image
         thread_signal();
         edit_addhist("+Brite:%.3f +R:%.3f +G:%.3f +B:%.3f "                     //  edit params > edit hist
                      "Con:%.3f R:%.3f G:%.3f B:%.3f",
                           inputs[0],inputs[1],inputs[2],inputs[3],
                           inputs[4],inputs[5],inputs[6],inputs[7]);
         edit_done(0);                                                           //  commit edit
         return 1;
      }

      edit_cancel(0);                                                            //  discard edit
      return 1;
   }

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

   if (strstr("BriteDens RedDens GreenDens BlueDens"
              "Contrast RedCon GreenCon BlueCon paint apply", event))
   {
      zdialog_fetch(zd,"BriteDens",inputs[0]);                                   //  get all inputs
      zdialog_fetch(zd,"RedDens",inputs[1]);
      zdialog_fetch(zd,"GreenDens",inputs[2]);
      zdialog_fetch(zd,"BlueDens",inputs[3]);
      zdialog_fetch(zd,"Contrast",inputs[4]);
      zdialog_fetch(zd,"RedCon",inputs[5]);
      zdialog_fetch(zd,"GreenCon",inputs[6]);
      zdialog_fetch(zd,"BlueCon",inputs[7]);

      thread_signal();                                                           //  trigger update thread
   }

   return 1;
}


//  thread function - multiple working threads to update image

void * RGB_thread(void *)
{
   using namespace adjust_RGB_names;

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

   if (Fpaintedits && ! Mdrag) return 0;                                         //  if mouse paint, must be drag

   if (sa_stat == sa_stat_fini) progress_setgoal(sa_Npixel);                     //  initz. progress counter
   else  progress_setgoal(Eww * Ehh);

   get_edit_pixels_init(NSMP,0);

   do_wthreads(RGB_wthread,NSMP);                                                //  worker threads

   progress_setgoal(0);

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

   if (! Fpaintedits) Fpaint2();                                                 //  update window
   return 0;
}


//  worker thread function

void * RGB_wthread(void *arg)
{
   using namespace adjust_RGB_names;

   float    R, G, B, R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float    briA, briR, briG, briB;
   float    conA, conR, conG, conB;

   int      index = *((int *) (arg));
   int      px, py, Fend;
   float    *pix1, *pix3;
   float    blend;

   briA = inputs[0];                                                             //  color brightness inputs, -1 to +1
   briR = inputs[1];
   briG = inputs[2];
   briB = inputs[3];

   R = briR - 0.5 * briG - 0.5 * briB + briA;                                    //  red = red - green - blue + all
   G = briG - 0.5 * briR - 0.5 * briB + briA;                                    //  etc.
   B = briB - 0.5 * briR - 0.5 * briG + briA;

   R += 1;                                                                       //  -1 ... 0 ... +1  >>  0 ... 1 ... 2
   G += 1;                                                                       //  increase the range
   B += 1;

   briR = R;                                                                     //  final color brightness factors
   briG = G;
   briB = B;

   conA = inputs[4];                                                             //  contrast inputs, -1 to +1
   conR = inputs[5];
   conG = inputs[6];
   conB = inputs[7];

   if (conA < 0) conA = 0.5 * conA + 1;                                          //  -1 ... 0  >>  0.5 ... 1.0
   else conA = conA + 1;                                                         //   0 ... 1  >>  1.0 ... 2.0
   if (conR < 0) conR = 0.5 * conR + 1;
   else conR = conR + 1;
   if (conG < 0) conG = 0.5 * conG + 1;
   else conG = conG + 1;
   if (conB < 0) conB = 0.5 * conB + 1;
   else conB = conB + 1;

   conR = conR * conA;                                                           //  apply overall contrast
   conG = conG * conA;
   conB = conB * conA;

   while (true)
   {
      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) return 0;
      if (blend == 0) continue;

      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      R1 = pix1[0];                                                              //  input RGB values, 0-255
      G1 = pix1[1];
      B1 = pix1[2];

      R3 = pix3[0];                                                              //  current output image RGB
      G3 = pix3[1];
      B3 = pix3[2];

      R9 = R1 * briR;                                                            //  apply color brightness factors
      G9 = G1 * briG;
      B9 = B1 * briB;

      R9 = conR * (R9 - 128) + 128;                                              //  apply contrast factors
      G9 = conG * (G9 - 128) + 128;
      B9 = conB * (B9 - 128) + 128;

      RGBfix(R9,G9,B9);

      if (Fpaintedits)                                                           //  gradual edit within mouse circle
      {
         if (blend > 0)
         {                                                                       //  increase edit
            R3 = blend * R9 + (1-blend) * R3;
            G3 = blend * G9 + (1-blend) * G3;
            B3 = blend * B9 + (1-blend) * B3;
         }

         else if (blend < 0)
         {                                                                       //  decrease edit
            R3 = -blend * R1 + (1+blend) * R3;
            G3 = -blend * G1 + (1+blend) * G3;
            B3 = -blend * B1 + (1+blend) * B3;
         }
      }

      else                                                                       //  full edit for image or area
      {
         R3 = blend * R9 + (1-blend) * R1;
         G3 = blend * G9 + (1-blend) * G1;
         B3 = blend * B9 + (1-blend) * B1;
      }

      pix3[0] = R3;                                                              //  output RGB values
      pix3[1] = G3;
      pix3[2] = B3;
   }

   return 0;
}


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

//  HSL color menu function
//  Adjust colors using the HSL (hue/saturation/lightness) color model

namespace adjust_HSL_names
{
   GtkWidget   *RGBframe, *RGBcolor;
   GtkWidget   *Hframe, *Hscale;
   editfunc    EF_HSL;                                                           //  edit function data
   int         Huse, Suse, Luse;                                                 //  "match using" flags, 0 or 1
   int         Hout, Sout, Lout;                                                 //  "output color" flags, 0 or 1
   float       Rm, Gm, Bm;                                                       //  RGB image color to match
   float       Hm, Sm, Lm;                                                       //  corresp. HSL color
   float       Mlev;                                                             //  match level 0..1 = 100%
   float       Hc, Sc, Lc;                                                       //  new color to add
   float       Rc, Gc, Bc;                                                       //  corresp. RGB color
   float       Adj;                                                              //  color adjustment, 0..1 = 100%
}


//  menu function

void m_adjust_HSL(GtkWidget *, ch *menu)
{
   using namespace adjust_HSL_names;

   void   HSL_RGBcolor(GtkWidget *drawarea, cairo_t *cr, int *);
   void   HSL_Hscale(GtkWidget *drawarea, cairo_t *cr, int *);
   int    HSL_dialog_event(zdialog *zd, ch *event);
   void   HSL_mousefunc();
   void * HSL_thread(void *);

   F1_help_topic = "adjust HSL";

   Plog(1,"m_adjust_HSL \n");

   EF_HSL.menuname = "Adjust HSL";
   EF_HSL.menufunc = m_adjust_HSL;
   EF_HSL.FprevReq = 1;                                                          //  use preview
   EF_HSL.Farea = 2;                                                             //  select area usable
   EF_HSL.Fpaintedits = 1;                                                       //  use with paint edits OK
   EF_HSL.mousefunc = HSL_mousefunc;                                             //  mouse function
   EF_HSL.threadfunc = HSL_thread;                                               //  thread function

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

/***
       ___________________________________________________
      |                                                   |
      |  Input color to match and adjust: [#####]         |
      |  Match using: [] Hue  [] Saturation  [] Lightness |
      |  Match Level: ==================[]========== 100% |
      |  - - - - - - - - - - - - - - - - - - - - - - - -  |
      |  Output Color                                     |
      |  [########]  [#################################]  |                      //  new color and hue spectrum
      |  [] Color Hue   ================[]==============  |
      |  [] Saturation  =====================[]=========  |
      |  [] Lightness   ===========[]===================  |
      |  Adjustment  ===================[]========== 100% |
      |                                                   |
      |                                  [Reset] [OK] [X] |
      |___________________________________________________|

***/

   zdialog *zd = zdialog_new("Adjust HSL",Mwin,"Reset","OK"," X ",null);
   EF_HSL.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"label","labmatch","hb1","Input color to match and adjust:","space=5");
   zdialog_add_widget(zd,"colorbutt","matchRGB","hb1","0|0|0");
   zdialog_add_ttip(zd,"matchRGB","shift+click on image to select color");

   zdialog_add_widget(zd,"hbox","hbmu","dialog");
   zdialog_add_widget(zd,"label","labmu","hbmu","Match using:","space=5");
   zdialog_add_widget(zd,"check","Huse","hbmu","Hue","space=3");
   zdialog_add_widget(zd,"check","Suse","hbmu","Saturation","space=3");
   zdialog_add_widget(zd,"check","Luse","hbmu","Lightness","space=3");

   zdialog_add_widget(zd,"hbox","hbmatch","dialog");
   zdialog_add_widget(zd,"label","labmatch","hbmatch","Match Level","space=5");
   zdialog_add_widget(zd,"hscale","Mlev","hbmatch","0|1|0.001|1.0","expand");
   zdialog_add_widget(zd,"label","lab100%","hbmatch","100%","space=4");

   zdialog_add_widget(zd,"hsep","sep","dialog",0,"space=5");

   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"label","laboutput","hb1","Output Color");

   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb2",0,"homog");
   zdialog_add_widget(zd,"vbox","vb2","hb2",0,"homog|expand");

   zdialog_add_widget(zd,"frame","RGBframe","vb1",0,"space=1");                  //  drawing area for RGB color
   RGBframe = zdialog_gtkwidget(zd,"RGBframe");
   RGBcolor = gtk_drawing_area_new();
   gtk_container_add(GTK_CONTAINER(RGBframe),RGBcolor);
   gtk_widget_set_size_request(RGBcolor,0,16);
   G_SIGNAL(RGBcolor,"draw",HSL_RGBcolor,0);

   zdialog_add_widget(zd,"frame","Hframe","vb2",0,"space=1");                    //  drawing area for hue scale
   Hframe = zdialog_gtkwidget(zd,"Hframe");
   Hscale = gtk_drawing_area_new();
   gtk_container_add(GTK_CONTAINER(Hframe),Hscale);
   gtk_widget_set_size_request(Hscale,200,16);
   G_SIGNAL(Hscale,"draw",HSL_Hscale,0);

   zdialog_add_widget(zd,"check","Hout","vb1","Color Hue");
   zdialog_add_widget(zd,"check","Sout","vb1","Saturation");
   zdialog_add_widget(zd,"check","Lout","vb1","Lightness");
   zdialog_add_widget(zd,"label","labadjust","vb1","Adjustment");

   zdialog_add_widget(zd,"hscale","Hc","vb2","0|359.9|0.1|180","expand");
   zdialog_add_widget(zd,"hscale","Sc","vb2","0|1|0.001|0.5","expand");
   zdialog_add_widget(zd,"hscale","Lc","vb2","0|1|0.001|0.5","expand");
   zdialog_add_widget(zd,"hbox","vb2hb","vb2");
   zdialog_add_widget(zd,"hscale","Adj","vb2hb","0|1|0.001|0.0","expand");
   zdialog_add_widget(zd,"label","lab100%","vb2hb","100%","space=4");

   zdialog_stuff(zd,"Huse",1);                                                   //  default: match on hue and saturation
   zdialog_stuff(zd,"Suse",1);
   zdialog_stuff(zd,"Luse",0);
   zdialog_stuff(zd,"Hout",1);                                                   //  default: replace only hue
   zdialog_stuff(zd,"Sout",0);
   zdialog_stuff(zd,"Lout",0);

   Rm = Gm = Bm = 0;                                                             //  color to match = black = not set
   Hm = Sm = Lm = 0;
   Huse = Suse = 1;
   Luse = 0;
   Hout = 1;
   Sout = Lout = 0;
   Mlev = 1.0;
   Hc = 180;                                                                     //  new HSL color to set / mix
   Sc = 0.5;
   Lc = 0.5;
   Adj = 0.0;

   zdialog_run(zd,HSL_dialog_event,"save");                                      //  run dialog - parallel
   takeMouse(HSL_mousefunc,arrowcursor);                                         //  connect mouse function
   return;
}


//  Paint RGBcolor drawing area with RGB color from new HSL color

void HSL_RGBcolor(GtkWidget *drawarea, cairo_t *cr, int *)
{
   using namespace adjust_HSL_names;

   int      ww, hh;

   ww = gtk_widget_get_allocated_width(drawarea);                                //  drawing area size
   hh = gtk_widget_get_allocated_height(drawarea);

   HSLtoRGB(Hc,Sc,Lc,Rc,Gc,Bc);                                                  //  new RGB color

   cairo_set_source_rgb(cr,Rc,Gc,Bc);
   cairo_rectangle(cr,0,0,ww-1,hh-1);
   cairo_fill(cr);

   return;
}


//  Paint Hscale drawing area with all hue values in a horizontal scale

void HSL_Hscale(GtkWidget *drawarea, cairo_t *cr, int *)
{
   using namespace adjust_HSL_names;

   int      px, ww, hh;
   float    H, S, L, R, G, B;

   ww = gtk_widget_get_allocated_width(drawarea);                                //  drawing area size
   hh = gtk_widget_get_allocated_height(drawarea);

   S = L = 0.5;

   for (px = 0; px < ww; px++)                                                   //  paint hue color scale
   {
      H = 360 * px / ww;
      HSLtoRGB(H,S,L,R,G,B);
      cairo_set_source_rgb(cr,R,G,B);
      cairo_move_to(cr,px,0);
      cairo_line_to(cr,px,hh-1);
      cairo_stroke(cr);
   }

   return;
}


//  HSL dialog event and completion function

int HSL_dialog_event(zdialog *zd, ch *event)                                     //  HSL dialog event function
{
   using namespace adjust_HSL_names;

   void   HSL_mousefunc();

   int      mod = 0;

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

   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();
      thread_signal();
      thread_wait();                                                             //  required for paint edits
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  reset
         zd->zstat = 0;                                                          //  keep dialog active
         Mlev = 1.0;
         Hc = 180;                                                               //  set defaults
         Sc = 0.5;
         Lc = 0.5;
         Adj = 0.0;
         zdialog_stuff(zd,"Mlev",Mlev);
         zdialog_stuff(zd,"Hc",Hc);
         zdialog_stuff(zd,"Sc",Sc);
         zdialog_stuff(zd,"Lc",Lc);
         zdialog_stuff(zd,"Adj",Adj);
         edit_reset();
         return 1;
      }
      else if (zd->zstat == 2) {                                                 //  done
         edit_fullsize();                                                        //  get full size image
         thread_signal();
         edit_done(0);                                                           //  commit edit
         return 1;
      }
      else {
         edit_cancel(0);                                                         //  discard edit
         return 1;
      }
   }

   if (strmatch("focus",event)) {
      takeMouse(HSL_mousefunc,arrowcursor);
      return 1;
   }

   if (strmatch(event,"Huse")) {                                                 //  match on Hue, 0/1
      zdialog_fetch(zd,"Huse",Huse);
      mod = 1;
   }

   if (strmatch(event,"Suse")) {                                                 //  match on Saturation, 0/1
      zdialog_fetch(zd,"Suse",Suse);
      mod = 1;
   }

   if (strmatch(event,"Luse")) {                                                 //  match on Lightness, 0/1
      zdialog_fetch(zd,"Luse",Luse);
      mod = 1;
   }

   if (strmatch(event,"Hout")) {                                                 //  replace Hue, 0/1
      zdialog_fetch(zd,"Hout",Hout);
      mod = 1;
   }

   if (strmatch(event,"Sout")) {                                                 //  replace Saturation, 0/1
      zdialog_fetch(zd,"Sout",Sout);
      mod = 1;
   }

   if (strmatch(event,"Lout")) {                                                 //  replace Lightness, 0/1
      zdialog_fetch(zd,"Lout",Lout);
      mod = 1;
   }

   if (strmatch("Mlev",event)) {                                                 //  color match 0..1 = 100%
      zdialog_fetch(zd,"Mlev",Mlev);
      mod = 1;
   }

   if (strmatch("Hc",event)) {                                                   //  new color hue 0-360
      zdialog_fetch(zd,"Hc",Hc);
      mod = 1;
   }

   if (strmatch("Sc",event)) {                                                   //  saturation 0-1
      zdialog_fetch(zd,"Sc",Sc);
      mod = 1;
   }

   if (strmatch("Lc",event)) {                                                   //  lightness 0-1
      zdialog_fetch(zd,"Lc",Lc);
      mod = 1;
   }

   if (strmatch("Adj",event)) {                                                  //  adjustment 0..1 = 100%
      zdialog_fetch(zd,"Adj",Adj);
      mod = 1;
   }

   if (strmatch(event,"paint")) mod = 1;                                         //  mouse paint

   if (mod) {
      gtk_widget_queue_draw(RGBcolor);                                           //  draw current RGB color
      thread_signal();                                                           //  trigger update thread
   }

   return 1;
}


//  mouse function
//  click on image to set the color to match and change

void HSL_mousefunc()
{
   using namespace adjust_HSL_names;

   int         mx, my, px, py;
   ch          color[20];
   float       *pix1, R, G, B;
   float       f256 = 1.0 / 256.0;
   zdialog     *zd = EF_HSL.zd;

   if (! KBshiftkey) return;                                                     //  check shift + left or right click
   if (! LMclick && ! RMclick) return;

   mx = Mxclick;                                                                 //  clicked pixel on image
   my = Myclick;

   if (mx < 1) mx = 1;                                                           //  pull back from image edge
   if (mx > Eww - 2) mx = Eww - 2;
   if (my < 1) my = 1;
   if (my > Ehh - 2) my = Ehh - 2;

   R = G = B = 0;

   for (py = my-1; py <= my+1; py++)                                             //  compute average RGB for 3x3
   for (px = mx-1; px <= mx+1; px++)                                             //    block of pixels
   {
      pix1 = PXMpix(E1pxm,px,py);
      R += pix1[0];
      G += pix1[1];
      B += pix1[2];
   }

   R = R / 9;
   G = G / 9;
   B = B / 9;

   if (LMclick)                                                                  //  left mouse click
   {                                                                             //  pick MATCH color from image
      LMclick = 0;
      snprintf(color,19,"%.0f|%.0f|%.0f",R,G,B);                                 //  draw new match color button
      if (zd) zdialog_stuff(zd,"matchRGB",color);
      Rm = R * f256;
      Gm = G * f256;
      Bm = B * f256;
      RGBtoHSL(Rm,Gm,Bm,Hm,Sm,Lm);                                               //  set HSL color to match
      thread_signal();                                                           //  trigger update thread
   }

   if (RMclick)                                                                  //  right mouse click
   {                                                                             //  pick OUTPUT color from image
      RMclick = 0;
      R = R * f256;
      G = G * f256;
      B = B * f256;
      RGBtoHSL(R,G,B,Hc,Sc,Lc);                                                  //  output HSL
      zdialog_stuff(zd,"Hc",Hc);
      zdialog_stuff(zd,"Sc",Sc);
      zdialog_stuff(zd,"Lc",Lc);
      gtk_widget_queue_draw(RGBcolor);                                           //  draw current RGB color
      thread_signal();                                                           //  trigger update thread
   }

   return;
}


//  thread function - multiple working threads to update image

void * HSL_thread(void *)
{
   using namespace adjust_HSL_names;

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

   if (Fpaintedits && ! Mdrag) return 0;                                         //  if mouse paint, must be drag 

   get_edit_pixels_init(NSMP,0);                                                 //  initz. pixel loop

   do_wthreads(HSL_wthread,NSMP);                                                //  worker threads

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

   if (! Fpaintedits) Fpaint2();                                                 //  update window
   return 0;
}


//  worker thread function

void * HSL_wthread(void *arg)
{
   using namespace adjust_HSL_names;

   int      index = *((int *) (arg));
   int      px, py, Fend;
   float    *pix1, *pix3;
   float    R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float    H1, S1, L1, H3, S3, L3;
   float    dH, dS, dL, match;
   float    a1, a2, blend;
   float    f256 = 1.0 / 256.0;

   while (true)
   {
      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) break;
      if (blend == 0) continue;

      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      R1 = f256 * pix1[0];                                                       //  input pixel RGB scaled 0-1
      G1 = f256 * pix1[1];
      B1 = f256 * pix1[2];

      R3 = pix3[0];                                                              //  current output image RGB
      G3 = pix3[1];
      B3 = pix3[2];

      RGBtoHSL(R1,G1,B1,H1,S1,L1);                                               //  convert to HSL

      match = 1.0;                                                               //  compare image pixel to match HSL

      if (Huse) {
         dH = fabsf(Hm - H1);
         if (360 - dH < dH) dH = 360 - dH;
         dH *= 0.002778;                                                         //  H difference, normalized 0..1
         match = 1.0 - dH;
      }

      if (Suse) {
         dS = fabsf(Sm - S1);                                                    //  S difference, 0..1
         match *= (1.0 - dS);
      }

      if (Luse) {
         dL = fabsf(Lm - L1);                                                    //  L difference, 0..1
         match *= (1.0 - dL);
      }

      a1 = pow(match, 10.0 * Mlev);                                              //  color selectivity, 0..1 = max
      a1 = Adj * a1;
      a2 = 1.0 - a1;

      if (Hout) H3 = a1 * Hc + a2 * H1;                                          //  output HSL = a1 * new HSL
      else H3 = H1;                                                              //             + a2 * old HSL
      if (Sout) S3 = a1 * Sc + a2 * S1;
      else S3 = S1;
      if (Lout) L3 = a1 * Lc + a2 * L1;
      else L3 = L1;

      HSLtoRGB(H3,S3,L3,R9,G9,B9);                                               //  conv. back to RGB 0-1

      R1 *= 255.9;                                                               //  rescale 0-255.9
      G1 *= 255.9;
      B1 *= 255.9;

      R9 *= 255.9;
      G9 *= 255.9;
      B9 *= 255.9;

      if (Fpaintedits)                                                           //  gradual edit within mouse circle
      {
         if (blend > 0)
         {                                                                       //  increase edit
            R3 = blend * R9 + (1-blend) * R3;
            G3 = blend * G9 + (1-blend) * G3;
            B3 = blend * B9 + (1-blend) * B3;
         }

         else if (blend < 0)
         {                                                                       //  decrease edit
            R3 = -blend * R1 + (1+blend) * R3;
            G3 = -blend * G1 + (1+blend) * G3;
            B3 = -blend * B1 + (1+blend) * B3;
         }
      }

      else                                                                       //  full edit for image or area
      {
         R3 = blend * R9 + (1-blend) * R1;
         G3 = blend * G9 + (1-blend) * G1;
         B3 = blend * B9 + (1-blend) * B1;
      }

      pix3[0] = R3;                                                              //  set output pixel
      pix3[1] = G3;
      pix3[2] = B3;
   }

   return 0;
}


//  HSL to RGB converter (Wikipedia)
//  H = 0-360 deg.  S = 0-1   L = 0-1
//  output RGB values = 0-1

void HSLtoRGB(float H, float S, float L, float &R, float &G, float &B)
{
   float    C, X, M;
   float    h1, h2;

   h1 = H / 60;
   h2 = h1 - 2 * int(h1/2);

   C = (1 - fabsf(2*L-1)) * S;
   X = C * (1 - fabsf(h2-1));
   M = L - C/2;

   if (H < 60) {
      R = C;
      G = X;
      B = 0;
   }

   else if (H < 120) {
      R = X;
      G = C;
      B = 0;
   }

   else if (H < 180) {
      R = 0;
      G = C;
      B = X;
   }

   else if (H < 240) {
      R = 0;
      G = X;
      B = C;
   }

   else if (H < 300) {
      R = X;
      G = 0;
      B = C;
   }

   else {
      R = C;
      G = 0;
      B = X;
   }

   R = R + M;
   G = G + M;
   B = B + M;

   if (R < 0) R = 0;
   if (G < 0) G = 0;
   if (B < 0) B = 0;

   if (R > 255.0) R = 255.0;
   if (G > 255.0) G = 255.0;
   if (B > 255.0) B = 255.0;

   return;
}


//  RGB to HSL converter
//  input RGB values 0-1
//  outputs: H = 0-360 deg.  S = 0-1   L = 0-1

void RGBtoHSL(float R, float G, float B, float &H, float &S, float &L)
{
   float    max, min, D;

   max = R;
   if (G > max) max = G;
   if (B > max) max = B;

   min = R;
   if (G < min) min = G;
   if (B < min) min = B;

   D = max - min;

   L = 0.5 * (max + min);

   if (D < 0.004) {
      H = S = 0;
      return;
   }

   if (L > 0.5)
      S = D / (2 - max - min);
   else
      S = D / (max + min);

   if (max == R)
      H = (G - B) / D;
   else if (max == G)
      H = 2 + (B - R) / D;
   else
      H = 4 + (R - G) / D;

   H = H * 60;
   if (H < 0) H += 360;

   return;
}


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

//  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 committed
//  image 9   committed dust removals + pending removal (work in process)

namespace dust_names
{
   editfunc    EFdust;

   int         spotspan;                                                         //  max. dustspot span, pixels
   int         spotspan2;                                                        //  spotspan **2
   float       brightness;                                                       //  brightness limit, 0 to 1 = white
   float       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
   float       groupbright[maxgroups];
   int         edgecount[maxgroups];                                             //  group edge pixel count
   float       edgebright[maxgroups];                                            //  group edge pixel brightness sum

   typedef struct {
      uint16      px1, py1, px2, py2;                                            //  pixel group extreme pixels
      int         span2;                                                         //  span from px1/py1 to px2/py2
   }  sgroupspan;

   sgroupspan    groupspan[maxgroups];
}


//  menu function

void m_remove_dust(GtkWidget *, ch *menu)
{
   using namespace dust_names;

   int    dust_dialog_event(zdialog *zd, ch *event);
   void * dust_thread(void *);

   F1_help_topic = "remove dust";

   Plog(1,"m_remove_dust \n");

   EFdust.menufunc = m_remove_dust;
   EFdust.menuname = "Remove Dust";
   EFdust.Farea = 2;                                                             //  select area usable
   EFdust.threadfunc = dust_thread;                                              //  thread function

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

   E9pxm = PXM_copy(E3pxm);                                                      //  image 9 = copy of image3
   Fred = 0;

   int cc = E1pxm->ww * E1pxm->hh * sizeof(int);
   pixgroup = (int *) zmalloc(cc,"remove dust");                                 //  maps pixels to assigned groups

   cc = E1pxm->ww * E1pxm->hh * sizeof(spixstack);
   pixstack = (spixstack *) zmalloc(cc,"remove dust");                           //  pixel group search stack

/***
       ____________________________________________
      |                Remove Dust                 |
      |                                            |
      | spot size limit    =========[]===========  |
      | max. brightness    =============[]=======  |
      | min. contrast      ========[]============  |
      | [erase] [red] [undo last] [apply]          |
      |                                            |
      |                                   [OK] [X] |
      |____________________________________________|

***/

   zdialog *zd = zdialog_new("Remove Dust",Mwin,"OK"," X ",null);
   EFdust.zd = zd;

   zdialog_add_widget(zd,"hbox","hbssl","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labssl","hbssl","spot size limit","space=5");
   zdialog_add_widget(zd,"hscale","spotspan","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","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","min. contrast","space=5");
   zdialog_add_widget(zd,"hscale","contrast","hbmc","1|500|1|40","space=5|expand");
   zdialog_add_widget(zd,"hbox","hbbutts","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","erase","hbbutts","Erase","space=5");
   zdialog_add_widget(zd,"button","red","hbbutts","Red","space=5");
   zdialog_add_widget(zd,"button","undo1","hbbutts","Undo Last","space=5");
   zdialog_add_widget(zd,"button","apply","hbbutts","Apply","space=5");

   zdialog_resize(zd,300,0);
   zdialog_load_inputs(zd);                                                      //  preload prior user inputs

   zdialog_fetch(zd,"spotspan",spotspan);                                        //  max. dustspot span (pixels)
   spotspan2 = spotspan * spotspan;

   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_run(zd,dust_dialog_event,"save");                                     //  run dialog - parallel

   thread_signal();
   return;
}


//  dialog event and completion callback function

int dust_dialog_event(zdialog *zd, ch *event)
{
   using namespace dust_names;

   void dust_erase();

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  done
         thread_wait();                                                          //  wait for thread
         PXM_free(E3pxm);
         E3pxm = E9pxm;                                                          //  image 3 = image 9
         E9pxm = 0;
         edit_done(0);                                                           //  commit edit
      }
      else {                                                                     //  cancel
         PXM_free(E9pxm);
         edit_cancel(0);                                                         //  discard edit
      }
      zfree(pixgroup);                                                           //  free memory
      zfree(pixstack);
      return 1;
   }

   if (zstrstr("spotspan brightness contrast red",event))
   {
      zdialog_fetch(zd,"spotspan",spotspan);                                     //  max. dustspot span (pixels)
      spotspan2 = spotspan * spotspan;

      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

      thread_signal();                                                           //  do the work
   }

   if (strmatch(event,"erase")) dust_erase();

   if (strmatch(event,"undo1")) {
      PXM_free(E3pxm);
      E3pxm = PXM_copy(E9pxm);
      Fred = 0;
      Fpaint2();
   }

   if (strmatch(event,"apply")) {                                                //  button
      if (Fred) dust_erase();
      PXM_free(E9pxm);                                                           //  image 9 = copy of image 3
      E9pxm = PXM_copy(E3pxm);
      CEF->Fmods++;
      CEF->Fsaved = 0;
   }

   return 1;
}


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

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

   int         xspan, yspan, span2;
   int         group, cc, ii, kk, Nremoved;
   int         px, py, dx, dy, ppx, ppy, npx, npy;
   float       gbright, pbright, pcontrast;
   float       ff = 1.0 / 256.0;
   uint16      direc;
   float       *pix3;

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

   cc = E1pxm->ww * E1pxm->hh * sizeof(int);                                     //  clear group arrays
   memset(pixgroup,0,cc);
   cc = maxgroups * sizeof(int);
   memset(groupcount,0,cc);
   memset(edgecount,0,cc);
   cc = maxgroups * sizeof(float );
   memset(groupbright,0,cc);
   memset(edgebright,0,cc);
   cc = maxgroups * sizeof(sgroupspan);
   memset(groupspan,0,cc);

   group = 0;

   for (py = 0; py < E1pxm->hh; py++)                                            //  loop all pixels
   for (px = 0; px < E1pxm->ww; px++)
   {
      ii = py * E1pxm->ww + px;
      if (sa_stat == sa_stat_fini && ! sa_pixmap[ii]) continue;                  //  not in active area
      if (pixgroup[ii]) continue;                                                //  already assigned to a group

      pix3 = PXMpix(E3pxm,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 error");
               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 error");
         }

         if (npx < 0 || npx > E1pxm->ww-1) continue;                             //  pixel off the edge
         if (npy < 0 || npy > E1pxm->hh-1) continue;

         ii = npy * E1pxm->ww + npx;
         if (pixgroup[ii]) continue;                                             //  pixel already assigned
         if (sa_stat == sa_stat_fini && ! sa_pixmap[ii]) continue;               //  pixel outside area

         pix3 = PXMpix(E3pxm,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 < E1pxm->hh; py++)                                            //  loop all pixels
   for (px = 0; px < E1pxm->ww; px++)
   {
      ii = py * E1pxm->ww + px;
      group = pixgroup[ii];
      if (! group) continue;
      if (groupspan[group].px1 == 0) {                                           //  first pixel found in this group
         groupspan[group].px1 = px;                                              //  group px1/py1 = this pixel
         groupspan[group].py1 = py;
         continue;
      }
      xspan = groupspan[group].px1 - px;                                         //  span from group px1/py1 to this pixel
      yspan = groupspan[group].py1 - py;
      span2 = xspan * xspan + yspan * yspan;
      if (span2 > groupspan[group].span2) {
         groupspan[group].span2 = span2;                                         //  if greater, group px2/py2 = this pixel
         groupspan[group].px2 = px;
         groupspan[group].py2 = py;
      }
   }

   for (py = 0; py < E1pxm->hh; py++)                                            //  loop all pixels
   for (px = 0; px < E1pxm->ww; px++)
   {
      ii = py * E1pxm->ww + px;
      group = pixgroup[ii];
      if (! group) continue;
      if (groupspan[group].span2 > spotspan2) continue;
      xspan = groupspan[group].px2 - px;                                         //  span from this pixel to group px2/py2
      yspan = groupspan[group].py2 - py;
      span2 = xspan * xspan + yspan * yspan;
      if (span2 > groupspan[group].span2) {
         groupspan[group].span2 = span2;                                         //  if greater, group px1/py1 = this pixel
         groupspan[group].px1 = px;
         groupspan[group].py1 = py;
      }
   }

   for (py = 0; py < E1pxm->hh; py++)                                            //  loop all pixels
   for (px = 0; px < E1pxm->ww; px++)
   {
      ii = py * E1pxm->ww + px;                                                  //  eliminate group if span > limit
      group = pixgroup[ii];
      if (! group) continue;
      if (! groupcount[group]) pixgroup[ii] = 0;
      else if (groupspan[group].span2 > spotspan2) {
         pixgroup[ii] = 0;
         groupcount[group] = 0;
         Nremoved++;
      }
   }

   for (py = 1; py < E1pxm->hh-1; py++)                                          //  loop all pixels except image edges
   for (px = 1; px < E1pxm->ww-1; px++)
   {
      ii = py * E1pxm->ww + px;
      group = pixgroup[ii];
      if (group) continue;                                                       //  find pixels bordering group pixels
      pix3 = PXMpix(E3pxm,px,py);
      pbright = ff * PIXBRIGHT(pix3);

      group = pixgroup[ii-E1pxm->ww-1];
      if (group) {
         ++edgecount[group];                                                     //  accumulate pixel count and
         edgebright[group] += pbright;                                           //      bordering the groups
      }

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

      group = pixgroup[ii-E1pxm->ww+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+E1pxm->ww-1];
      if (group) {
         ++edgecount[group];
         edgebright[group] += pbright;
      }

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

      group = pixgroup[ii+E1pxm->ww+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 < E1pxm->hh; py++)                                            //  loop all pixels
   for (px = 0; px < E1pxm->ww; px++)
   {
      ii = py * E1pxm->ww + px;                                                  //  eliminate group if low contrast
      group = pixgroup[ii];
      if (! group) continue;
      if (! groupcount[group]) pixgroup[ii] = 0;
   }

   for (py = 0; py < E1pxm->hh; py++)                                            //  loop all pixels
   for (px = 0; px < E1pxm->ww; px++)
   {
      ii = py * E1pxm->ww + px;
      if (! pixgroup[ii]) continue;                                              //  not a dust pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  paint it red
      pix3[0] = 255;
      pix3[1] = pix3[2] = 0;
   }

   Fred = 1;

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


//  erase the marked 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, dist2, mindist2;
   float       slope;
   float       *pix1, *pix3;
   ch          *pmap;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   Funcbusy(+1);
   PXM_free(E3pxm);                                                              //  not a thread
   E3pxm = PXM_copy(E9pxm);

   cc = E1pxm->ww * E1pxm->hh;                                                   //  allocate pixel done map
   pmap = (ch *) zmalloc(cc,"remove dust");

   for (py = 0; py < E1pxm->hh; py++)                                            //  loop all pixels
   for (px = 0; px < E1pxm->ww; px++)
   {
      ii = py * E1pxm->ww + 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 >= E1pxm->ww) continue;                             //  off image edge
            if (qy < 0 || qy >= E1pxm->hh) continue;
            ii = qy * E1pxm->ww + 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 * E1pxm->ww + sx;
            if (pmap[ii]) continue;
            pmap[ii] = 1;
            tx = qx + (qx - sx);                                                 //  tx/ty = parallel line from qx/qy
            ty = qy + (qy - sy);
            if (tx < 0) tx = 0;
            if (tx > E1pxm->ww-1) tx = E1pxm->ww-1;
            if (ty < 0) ty = 0;
            if (ty > E1pxm->hh-1) ty = E1pxm->hh-1;
            pix1 = PXMpix(E3pxm,tx,ty);                                          //  copy pixel from tx/ty to sx/sy
            pix3 = PXMpix(E3pxm,sx,sy);
            memcpy(pix3,pix1,pcc);
         }
      }

      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 * E1pxm->ww + sx;
            if (pmap[ii]) continue;
            pmap[ii] = 1;
            tx = qx + (qx - sx);
            ty = qy + (qy - sy);
            if (tx < 0) tx = 0;
            if (tx > E1pxm->ww-1) tx = E1pxm->ww-1;
            if (ty < 0) ty = 0;
            if (ty > E1pxm->hh-1) ty = E1pxm->hh-1;
            pix1 = PXMpix(E3pxm,tx,ty);
            pix3 = PXMpix(E3pxm,sx,sy);
            memcpy(pix3,pix1,pcc);
         }
      }
   }

   zfree(pmap);

   Fred = 0;
   Funcbusy(-1);
   Fpaint2();                                                                    //  update window
   return;
}


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

//  Fix color fringes or lateral chromatic aberration
//  Shift R/B color planes to maximize overlap with G plane.
//  Radius of a pixel in R-plane or B-plane is shifted using the formula:
//    R2 = F1 * R1 + F2 * R1 * R1 + F3 * sqrt(R1)
//    R1: pixel old radius   R2: pixel new radius
//    F1 F2 F3: computed values for maximum overlap
//    F1 is near 1.0
//    F2 and F3 are near 0.0

namespace fringes_names
{
   editfunc    EFfringes;
   double      Rf1, Rf2, Rf3, Bf1, Bf2, Bf3;
   int         cx, cy;
   uint8       *pixcon;
   int         threshcon;
   int         evrgb;
   float       evF1, evF2, evF3;
   double      evRsum1[Xsmp];
   double      evRsum2;
}


//  menu function

void m_fringes(GtkWidget *, ch *menu)
{
   using namespace fringes_names;

   void  fringes_threshcon();
   int   fringes_dialog_event(zdialog* zd, ch *event);

   ch       *title = "Color Fringes";

   F1_help_topic = "fringes";

   Plog(1,"m_fringes \n");

   EFfringes.menuname = "Fringes";                                               //  setup edit
   EFfringes.menufunc = m_fringes;
   EFfringes.Farea = 2;                                                          //  select area usable 

   if (! edit_setup(EFfringes)) return;

   cx = Eww / 2;                                                                 //  image center
   cy = Ehh / 2;

   fringes_threshcon();                                                          //  get image threshold contrast

/***
       ____________________________________________
      |              Color Fringes                 |
      |                                            |
      |  Red Factors:   [ 1.0 ]  [ 0.0 ]  [ 0.0 ]  |
      |  Blue Factors:  [ 1.0 ]  [ 0.0 ]  [ 0.0 ]  |
      |                                            |
      |  Find optimum factors: [ Search ]          |
      |                                            |
      |                                   [OK] [X] |
      |____________________________________________|

***/

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

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vb3","hb1",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vb4","hb1",0,"space=3|homog");
   zdialog_add_widget(zd,"label","labR","vb1","Red Factors");
   zdialog_add_widget(zd,"zspin","Rf1","vb2","-3|+3|0.2|0.0","size=6");
   zdialog_add_widget(zd,"zspin","Rf2","vb3","-3|+3|0.2|0.0","size=6");
   zdialog_add_widget(zd,"zspin","Rf3","vb4","-3|+3|0.2|0.0","size=6");
   zdialog_add_widget(zd,"label","labB","vb1","Blue Factors");
   zdialog_add_widget(zd,"zspin","Bf1","vb2","-3|+3|0.2|0.0","size=6");
   zdialog_add_widget(zd,"zspin","Bf2","vb3","-3|+3|0.2|0.0","size=6");
   zdialog_add_widget(zd,"zspin","Bf3","vb4","-3|+3|0.2|0.0","size=6");
   zdialog_add_widget(zd,"hbox","hbopt","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labopt","hbopt","Find optimum factors:");
   zdialog_add_widget(zd,"button","search","hbopt","Search","space=5");

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


//  dialog event and completion function

int fringes_dialog_event(zdialog *zd, ch *event)
{
   using namespace fringes_names;

   void  fringes_RBshift();
   void  fringes_RBsearch();

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

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  done
      {
         edit_addhist("Red:%.1f %.1f %.1f Blue:%.1f %.1f %.1f",                  //  edit params > edit hist
                                 Rf1, Rf2, Rf3, Bf1, Bf2, Bf3);                  //  log metadata edit hist
         edit_done(0);
      }
      else edit_cancel(0);                                                       //  discard edit
      zfree(pixcon);                                                             //  free memory
      return 1;
   }

   if (zstrstr("Rf1 Rf2 Rf3 Bf1 Bf2 Bf3",event))
   {
      zdialog_fetch(zd,"Rf1",Rf1);                                               //  get manually adjusted factors
      zdialog_fetch(zd,"Rf2",Rf2);
      zdialog_fetch(zd,"Rf3",Rf3);
      zdialog_fetch(zd,"Bf1",Bf1);
      zdialog_fetch(zd,"Bf2",Bf2);
      zdialog_fetch(zd,"Bf3",Bf3);

      fringes_RBshift();                                                         //  shift R/B color planes using factors
   }

   if (strmatch(event,"search"))
   {
      fringes_RBsearch();                                                        //  search for optimum factors

      zdialog_stuff(zd,"Rf1",Rf1);                                               //  stuff dialog with found factors
      zdialog_stuff(zd,"Rf2",Rf2);
      zdialog_stuff(zd,"Rf3",Rf3);
      zdialog_stuff(zd,"Bf1",Bf1);
      zdialog_stuff(zd,"Bf2",Bf2);
      zdialog_stuff(zd,"Bf3",Bf3);

      fringes_RBshift();                                                         //  shift R/B color planes using factors
   }

   return 1;
}


//  Find contrast threshold for highest-contrast pixels in color green.

void fringes_threshcon()
{
   using namespace fringes_names;

   int      ii, jj;
   int      px, py, qx, qy;
   int      Gcon, Gmax;
   int      conhist[256];
   float    *pix1, *pix2;

   pixcon = (uint8 *) zmalloc(Eww * Ehh,"fringes");                              //  map of pixel green contrast

   for (py = 9; py < Ehh-9; py++)                                                //  loop all pixels
   for (px = 9; px < Eww-9; px++)
   {
      Gmax = 0;
      pix1 = PXMpix(E1pxm,px,py);

      for (qy = py - 5; qy <= py + 5; qy += 10)                                  //  loop 4 neighbors offset by 5
      for (qx = px - 5; qx <= px + 5; qx += 10)
      {
         pix2 = PXMpix(E1pxm,qx,qy);                                             //  find max. green contrast
         Gcon = pix1[1] - pix2[1];
         Gcon = abs(Gcon);
         if (Gcon > Gmax) Gmax = Gcon;
      }

      ii = py * Eww + px;                                                        //  save pixel green contrast
      pixcon[ii] = Gmax;
   }

   memset(conhist, 0, 256 * sizeof(int));

   for (ii = 0; ii < Eww * Ehh; ii++)                                            //  make histogram of contrast values
   {
      jj = pixcon[ii];                                                           //  conhist[jj] = pixels with
      ++conhist[jj];                                                             //    contrast value jj
   }

   for (jj = 0, ii = 255; ii > 0; ii--)                                          //  find minimum contrast for 200K
   {                                                                             //    highest green-contrast pixels
      jj += conhist[ii];
      if (ii > 100 && jj > 100000) break;
      if (jj > 200000) break;
   }

   threshcon = ii;                                                               //  threshold for fringes_evaluate()
   return;
}


//  Compute factors Rf1 Rf2 Bf1 Bf2 that minimize
//    Rplane - Gplane  and  Bplane - Gplane

void fringes_RBsearch()
{
   using namespace fringes_names;

   double fringes_evaluate(int rgb, float F1, float F2, float F3);

   int      rgb;                                                                 //  0/1/2 = red/green/blue
   float    F1, F2, F3;
   float    R1, R2, R3;
   float    S1, S2, S3;
   double   Rsum, Rmin;

   Funcbusy(+1);
   progress_setgoal(1372);                                                       //  initz. progress counter

   for (rgb = 0; rgb <= 2; rgb += 2)                                             //  loop rgb = 0, 2  (red, blue)
   {
      Rmin = 1.0 * Eww * Ehh * 256;                                              //  max. possible image difference
      R1 = R2 = R3 = 0;

      for (F1 = -3; F1 <= +3; F1 += 1.0)                                         //  loop all combinations F1, F2, F3
      for (F2 = -3; F2 <= +3; F2 += 1.0)                                         //    in 1-pixel steps
      for (F3 = -3; F3 <= +3; F3 += 1.0)
      {
         Rsum = fringes_evaluate(rgb, F1, F2, F3);                               //  evaluate each combination
         if (Rsum < Rmin) {
            Rmin = Rsum;                                                         //  remember best combination
            R1 = F1;
            R2 = F2;
            R3 = F3;
         }

         progress_addvalue(1);
         zmainloop();
      }

      S1 = R1; S2 = R2; S3 = R3;                                                 //  loop around best combination

      for (F1 = R1-1; F1 <= R1+1; F1 += 0.333)                                   //  loop all combinations F1, F2, F3
      for (F2 = R2-1; F2 <= R2+1; F2 += 0.333)                                   //    in 0.333 pixel steps
      for (F3 = R3-1; F3 <= R3+1; F3 += 0.333)
      {
         Rsum = fringes_evaluate(rgb, F1, F2, F3);                               //  evaluate each combination
         if (Rsum < Rmin) {
            Rmin = Rsum;                                                         //  remember best combination
            S1 = F1;
            S2 = F2;
            S3 = F3;
         }

         progress_addvalue(1);
         zmainloop();
      }

      if (rgb == 0) {
         Rf1 = S1;                                                               //  red plane factors
         Rf2 = S2;
         Rf3 = S3;
      }
      else {
         Bf1 = S1;                                                               //  blue plane factors
         Bf2 = S2;
         Bf3 = S3;
      }
   }

   Funcbusy(-1);
   progress_setgoal(0);
   return;
}


//  evaluate the alignment of a shifted R/B color plane with the G plane
//  R/B color plane is shifted using factors F1 F2 F3
//  rgb is 0/2 for R/B

double fringes_evaluate(int rgb, float F1, float F2, float F3)
{
   using namespace fringes_names;

   void * fringes_evaluate_wthread(void *);

   evrgb = rgb;                                                                  //  make args avail. for thread
   evF1 = F1;
   evF2 = F2;
   evF3 = F3;

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

   evRsum2 = 0;
   for (int ii = 0; ii < NSMP; ii++) evRsum2 += evRsum1[ii];
   return evRsum2;
}


void * fringes_evaluate_wthread(void *arg)                                       //  worker thread
{
   using namespace fringes_names;

   int      index = *((int *) arg);
   int      ii, px1, py1;
   float    fx1, fy1, fx2, fy2, fx3, fy3;
   float    *pix1, vpix[4];
   double   Rsum = 0;

   for (py1 = index + 9; py1 < Ehh-9; py1 += NSMP)                               //  loop all image pixels
   for (px1 = 9; px1 < Eww-9; px1++)
   {
      ii = py1 * Eww + px1;                                                      //  skip low contrast pixel
      if (pixcon[ii] < threshcon) continue;
      fx1 = 1.0 * (px1 - cx) / cx;                                               //  -1 to +1 at edges, 0 at center
      fy1 = 1.0 * (py1 - cy) / cy;
      fx2 = fx1 * fabsf(fx1);                                                    //  square, keep sign
      fy2 = fy1 * fabsf(fy1);
      fx3 = fx1 * fx2;                                                           //  cube
      fy3 = fy1 * fy2;
      fx1 = fx1 * evF1;                                                          //  * F1
      fy1 = fy1 * evF1;
      fx2 = fx2 * evF2;                                                          //  * F2
      fy2 = fy2 * evF2;
      fx3 = fx3 * evF3;                                                          //  * F3
      fy3 = fy3 * evF3;
      pix1 = PXMpix(E1pxm,px1,py1);                                              //  image unshifted pixel (G)
      vpixel(E1pxm, px1+fx1+fx2+fx3, py1+fy1+fy2+fy3, vpix);                     //  virtual pixel, shifted (R/B)
      Rsum += fabs(vpix[evrgb] - pix1[1]);                                       //  sum shifted R/B - unshifted G
   }

   evRsum1[index] = Rsum;

   return 0;
}


//  shift R/B color planes using factors Rf1 Rf2 Bf1 Bf2

void fringes_RBshift()
{
   void * fringes_RBshift_wthread(void *);

   do_wthreads(fringes_RBshift_wthread,NSMP);                                    //  do worker threads
   
   if (sa_stat == sa_stat_fini) sa_postfix();                                    //  if select area, restore unselected

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

   Fpaint2();                                                                    //  update window
   return;
}


void * fringes_RBshift_wthread(void *arg)                                        //  worker thread
{
   using namespace fringes_names;

   int      index = *((int *) arg);
   int      px3, py3, ok;
   float    px1, py1;
   float    fx1, fy1, fx2, fy2, fx3, fy3;
   float    *pix3, vpix[4];

   for (py3 = index; py3 < Ehh; py3 += NSMP)                                     //  loop all image pixels
   for (px3 = 0; px3 < Eww; px3++)
   {
      pix3 = PXMpix(E3pxm,px3,py3);                                              //  output pixel

      fx1 = 1.0 * (px3 - cx) / cx;                                               //  -1 to +1 at edges, 0 at center
      fy1 = 1.0 * (py3 - cy) / cy;
      fx2 = fx1 * fabsf(fx1);                                                    //  square, keep sign
      fy2 = fy1 * fabsf(fy1);
      fx3 = fx1 * fx2;                                                           //  cube
      fy3 = fy1 * fy2;

      px1 = px3 + Rf1 * fx1 + Rf2 * fx2 + Rf3 * fx3;                             //  red shift
      py1 = py3 + Rf1 * fy1 + Rf2 * fy2 + Rf3 * fy3;
      ok = vpixel(E1pxm,px1,py1,vpix);                                           //  red input pixel
      if (ok) pix3[0] = vpix[0];

      px1 = px3 + Bf1 * fx1 + Bf2 * fx2 + Bf3 * fx3;                             //  blue shift
      py1 = py3 + Bf1 * fy1 + Bf2 * fy2 + Bf3 * fy3;
      ok = vpixel(E1pxm,px1,py1,vpix);                                           //  blue input pixel
      if (ok) pix3[2] = vpix[2];
   }

   return 0;
}


