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

   Fotocx - edit photos and manage collections

   Copyright 2007-2026 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, various methods
   m_fix_motionblur        fix motion blur using Richardson-Lucy algorithm
   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_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 = TX("SHARPEN");

   printf("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(TX("Sharpen"),Mwin,TX("Reset"),TX("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",TX("unsharp mask"),"space=5");
   zdialog_add_widget(zd,"label","lab21","vb22",TX("Radius"));
   zdialog_add_widget(zd,"label","lab22","vb22",TX("Amount"));
   zdialog_add_widget(zd,"label","lab23","vb22",TX("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",TX("amplify contrast"),"space=5");
   zdialog_add_widget(zd,"label","lab32","vb32",TX("Amount"));
   zdialog_add_widget(zd,"label","lab33","vb32",TX("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",TX("Kuwahara"),"space=3");
   zdialog_add_widget(zd,"label","space","hbku",0,"expand");
   zdialog_add_widget(zd,"label","lab42","hbku",TX("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",TX("median diff"),"space=5");
   zdialog_add_widget(zd,"label","lab51","vb52",TX("Radius"));
   zdialog_add_widget(zd,"label","lab52","vb52",TX("dark"));
   zdialog_add_widget(zd,"label","lab53","vb52",TX("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",TX("Richardson-Lucy"),"space=5");
   zdialog_add_widget(zd,"label","lab62","vb62",TX("Radius"));
   zdialog_add_widget(zd,"label","lab63","vb62",TX("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",TX("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,TX("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,TX("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;
   int         pskip = 0;

   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;

      if (++pskip == 10) {                                                             //  every 10th pixel                      26.0
         pskip = 0;                                                                    //  (reduce call overhead) 
         progress_addvalue(index,10);                                                  //  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         pskip = 0;

   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;

      if (++pskip == 10) {                                                             //  every 10th pixel                      26.0
         pskip = 0;                                                                    //  (reduce call overhead) 
         progress_addvalue(index,10);                                                  //  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         pskip = 0;

   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;

      if (++pskip == 10) {                                                             //  every 10th pixel                      26.0
         pskip = 0;                                                                    //  (reduce call overhead) 
         progress_addvalue(index,10);                                                  //  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)
   int         pskip = 0;

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

      if (++pskip == 10) {                                                             //  every 10th pixel                      26.0
         pskip = 0;                                                                    //  (reduce call overhead) 
         progress_addvalue(index,10);                                                  //  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(1);                                                            //  track progress 

//  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(1);                                                            //  track progress
   }

   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;
   int      pskip = 0;

   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

         if (++pskip == 10) {                                                          //  every 10th pixel                      26.0
            pskip = 0;                                                                 //  (reduce call overhead) 
            progress_addvalue(index,10);                                               //  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;

         if (++pskip == 10) {                                                          //  every 10th pixel                      26.0
            pskip = 0;                                                                 //  (reduce call overhead) 
            progress_addvalue(index,10);                                               //  track progress
         }
      }
   }

   return 0;
}


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

//  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 = TX("Shift + drag mouse across image \n"
                        "to indicate blur direction");

   printf("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(TX("Fix Motion Blur"),Mwin,TX("Reset"),TX("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",TX("Blur Span (pixels)"));
   zdialog_add_widget(zd,"label","labangle","vb1",TX("Blur Angle (degrees)"));
   zdialog_add_widget(zd,"label","labiter","vb1",TX("Algorithm Iterations"));
   zdialog_add_widget(zd,"label","labsup","vb1",TX("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 (rows) 

      for (rgb = 0; rgb < 3; rgb++)                                                    //  loop RGB
      {
         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;
}


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

//  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 = TX("BLUR");

   printf("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   |
      |                                                                    |
      |                                                                [X] |
      |____________________________________________________________________|

***/

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

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

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

   return 1;
}


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

//  normal blur

namespace blur_normal_names
{
   editfunc    EFblur;
   PXM         *E2pxm;
   float       radius;                                                                 //  blur radius
   float       horz, vert;                                                             //  blur distance, horz and vert
   float       Wh[1000], Wv[1000];                                                     //  weights for radius < 1000
}


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 = TX("BLUR");

   printf("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
   EFblur.Fscript = 1;                                                                 //  allow scripting                       25.1

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

/***
          _________________________________________
         |            Normal Blur                  |
         |                                         |
         |  Radius [___]   Horz [___]  Vert [___]  |                                   //  separate horz and vert                25.1
         |                                         |
         |                [Reset] [Apply] [OK] [X] |
         |_________________________________________|

***/

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

   zdialog_add_widget(zd,"hbox","hbnb","dialog");
   zdialog_add_widget(zd,"label","labrad","hbnb",TX("Radius"),"space=3");
   zdialog_add_widget(zd,"zspin","radius","hbnb","1|999|1|10","space=3|size=3");
   zdialog_add_widget(zd,"label","labhorz","hbnb",TX("Horz"),"space=3");
   zdialog_add_widget(zd,"zspin","horz","hbnb","1|999|1|10","space=3|size=3");
   zdialog_add_widget(zd,"label","labvert","hbnb",TX("Vert"),"space=3");
   zdialog_add_widget(zd,"zspin","vert","hbnb","1|999|1|10","space=3|size=3");

   E2pxm = 0;

   zdialog_load_inputs(zd);                                                            //  restore inputs
   zdialog_fetch(zd,"radius",radius);
   zdialog_fetch(zd,"horz",horz);
   zdialog_fetch(zd,"vert",vert);

   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()
   if (strmatch(event,"apply")) zd->zstat = 2;                                         //  from script

   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",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();

   if (strmatch(event,"radius")) {
      zdialog_fetch(zd,"radius",radius);                                               //  radius input
      horz = vert = radius;                                                            //  horz and vert blur = same
      zdialog_stuff(zd,"horz",horz);
      zdialog_stuff(zd,"vert",vert);
   }

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

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

   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, N;
   float       rad, w, wsum;

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

   rad = horz;                                                                         //  horizontal blur
   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
      Wh[ii] = w;
      wsum += w;
   }

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

   rad = vert;                                                                         //  same for vertical blur
   wsum = 0;

   for (ii = 0; ii <= rad; ii++)
   {
      w = 1.0 - (ii + 0.25) / rad;
      w = w * w;
      Wv[ii] = w;
      wsum += w;
   }

   for (ii = 0; ii <= rad; ii++)
      Wv[ii] = Wv[ii] / wsum;

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

   if (! Fpaintedits)
   {
      N = 0;
      if (horz) N++;
      if (vert) N++;
      if (sa_stat == sa_stat_fini) progress_setgoal(sa_Npixel * N);                    //  initz. progress counter
      else  progress_setgoal(Eww * Ehh * N);
   }

   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 - vertical blur

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

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

   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 = Wv[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) {
         if (++pskip == 10) {                                                          //  every 10th pixel                      26.0
            pskip = 0;                                                                 //  (reduce call overhead) 
            progress_addvalue(index,10);                                               //  track progress
         }
      }
   }

   return 0;
}


//  worker thread 2 - horizontal blur

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

   int      index = *((int *) arg);
   int      rad = horz;                                                                //  25.1
   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;
   int      pskip = 0;

   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 = Wh[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) {
         if (++pskip == 10) {                                                          //  every 10th pixel                      26.0
            pskip = 0;                                                                 //  (reduce call overhead) 
            progress_addvalue(index,10);                                               //  track progress
         }
      }
   }

   return 0;
}


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

//  radial blur

namespace blur_radial_names
{
   editfunc    EFblurad;
   zdialog     *zd;                                                                    //  25.1
   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 = TX("BLUR");

   printf("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;                                         //  mouse function

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

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

***/

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

   zdialog_add_widget(zd,"hbox","hbrb2","dialog");
   zdialog_add_widget(zd,"label","labrbr","hbrb2",TX("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",TX("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",TX("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]
      {
         zdialog_fetch(zd,"Cx",Cx);                                                    //  make scriptable                       25.1
         zdialog_fetch(zd,"Cy",Cy);
         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;
   int      pskip = 0;

   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;

      if (++pskip == 10) {                                                             //  every 10th pixel                      26.0
         pskip = 0;                                                                    //  (reduce call overhead) 
         progress_addvalue(index,10);                                                  //  track progress
      }
   }

   return 0;
}


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

//  directed blur - blur in direction of mouse drag

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 = TX("BLUR");

   printf("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(TX("Directed Blur"),Mwin,TX("Reset"),"OK","X",null);
   EFblurdirc.zd = zd;

   zdialog_add_widget(zd,"hbox","hbdb2","dialog");
   zdialog_add_widget(zd,"label","labspan","hbdb2",TX("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",TX("Intensity"));
   zdialog_add_widget(zd,"zspin","intens","hbdb2","0.00|1.0|0.01|0.1","space=3|size=3");

   Dspan = 0.1;
   Dintens = 0.1;

   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 - blur inversly proportional to sharpness

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 = TX("BLUR");

   printf("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
   EFblurgrad.Fscript = 1;                                                             //  allow scripting                       25.1

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

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

***/

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

   zdialog_add_widget(zd,"hbox","hbgb2","dialog");
   zdialog_add_widget(zd,"label","labgrad","hbgb2",TX("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",TX("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_load_inputs(zd);
   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 (strmatch(event,"apply")) zd->zstat = 2;                                         //  from script                           25.1

   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,"radius",radius);                                                  //  for script                            25.1
   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;

   zadd_locked(NFbusy,+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
   }

   zadd_locked(NFbusy,-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;

   zadd_locked(NFbusy,+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");

   zadd_locked(NFbusy,-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;
   int      pskip = 0;

   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) {
         if (++pskip == 10) {                                                          //  every 10th pixel                      26.0
            pskip = 0;                                                                 //  (reduce call overhead) 
            progress_addvalue(index,10);                                               //  track progress
         }
         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) {
         if (++pskip == 10) {                                                          //  every 10th pixel                      26.0
            pskip = 0;                                                                 //  (reduce call overhead) 
            progress_addvalue(index,10);                                               //  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 *);

   printf("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(TX("Blur Background"),Mwin,TX("Apply"),"OK","X",null);
   CEF->zd = zd;

   zdialog_add_widget(zd,"hbox","hbcon","dialog",0,"space=5");
   zdialog_add_widget(zd,"check","conrad","hbcon",TX("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",TX("increase blur with distance"),"space=3");
   zdialog_add_widget(zd,"hbox","hbmin","dialog");
   zdialog_add_widget(zd,"label","labmin","hbmin",TX("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",TX("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,TX("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;
   int         pskip = 1;

   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;

      if (++pskip == 10) {                                                             //  every 10th pixel                      26.0
         pskip = 0;                                                                    //  (reduce call overhead) 
         progress_addvalue(index,10);                                                  //  track progress
      }
   }

   return 0;                                                                           //  exit thread
}


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

//  image noise reduction

namespace denoise_names
{
   cch      *dn_method;
   int      dn_radius;
   int      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_thread(void *);

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

   F1_help_topic = TX("DENOISE");

   printf("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 = 0;                                                              //  scripting NOT supported               25.1
   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 [_10_]  ignore differences above limit    |
         |                                                  |
         |  [median]  pixel = median of all neighbors       |
         |  [smooth]  pixel = mean of best match neighbors  |
         |                                                  |
         |                      [Measure] [Reset] [OK] [X]  |
         |__________________________________________________|

***/

   zdialog *zd = zdialog_new(TX("Noise Reduction"),Mwin,"Measure",TX("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=2");
   zdialog_add_widget(zd,"label","labrad","hbrad",TX("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",TX("scope of neighbor pixels"),"space=3");

   zdialog_add_widget(zd,"hbox","hblim","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","lablim","hblim",TX("Limit"),"space=3");
   zdialog_add_widget(zd,"zspin","dn_limit","hblim","1|255|1|100","size=4|space=3");
   zdialog_add_widget(zd,"label","lablim","hblim",TX("ignore differences above limit"),"space=3");

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

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

   zdialog_add_widget(zd,"hbox","hbsmoo","dialog",0,"space=2");
   zdialog_add_widget(zd,"button","smooth","hbsmoo",TX("smooth"),"space=3");
   zdialog_add_widget(zd,"label","labsmo","hbsmoo",TX("pixel = mean of best match neighbors"),"space=3");

   zdialog_load_inputs(zd);

   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);                                            //  get user inputs
   zdialog_fetch(zd,"dn_limit",dn_limit);

   if (strstr("median smooth",event)) {                                                //  get denoise method
      if (strmatch(event,"median")) dn_method = "median";                              //    and execute one iteration
      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;
}


//  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, samp;
   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;
   int         pskip = 0;

   rad = dn_radius;

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

   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 > samp / 2) break;                                              //    have been counted
         }
         
         median = ii;                                                                  //  median rgb for surrounding pixels
         
         if (abs(median - pix9[rgb]) > dn_limit) continue;                             //  ignore if large difference

         pix9[rgb] = median;                                                           //  moderate pixel 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;

      if (++pskip == 10) {                                                             //  every 10th pixel                      26.0
         pskip = 0;                                                                    //  (reduce call overhead) 
         progress_addvalue(index,10);                                                  //  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         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;
   int         pskip = 0;

   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 = 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 (abs(pix9[rgb] - mrgb) < dn_limit)                                         //  if difference within limit
            pix9[rgb] = mrgb;                                                          //    moderate pixel                      26.0
         else pix9[rgb] = pix8[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];

      }

      if (++pskip == 10) {                                                             //  every 10th pixel                      26.0
         pskip = 0;                                                                    //  (reduce call overhead) 
         progress_addvalue(index,10);                                                  //  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 = TX("Measure Noise");
   ch          *mousemess = TX("Move mouse in a monotone image area.");
   ch          *mformat = TX("mean RGB: %5.0f %5.0f %5.0f");
   ch          *nformat = TX("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",TX("Center"),"space=3");
   zdialog_add_widget(zd,"label","space","hbce",0,"expand");
   zdialog_add_widget(zd,"label","labend","hbce",TX("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 (NFbusy) 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 fog/haze from an image or selected areas

namespace defog_names
{
   editfunc    EFdefog;
   float       minusfog;                                                               //  fog reduction, 0 - 1
   float       plusbrite;                                                              //  brightness addition, 0 - 1
   float       minusblue;                                                              //  blue attenuation, 0 - 1
}


//  menu function

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

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

   F1_help_topic = TX("DEFOG");

   printf("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

/***
       ______________________________________
      |        Reduce Fog/Haze               |
      |                                      |
      |  - fog/haze ========[]=============  |        0 - 1     minusfog
      |  + bright ==============[]=========  |        0 - 1     plusbrite
      |  - blue  ==========[]==============  |        0 - 1     minusblue
      |                                      |
      |                     [Reset] [OK] [X] |
      |______________________________________|

***/

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

   zdialog_add_widget(zd,"hbox","hbdefog","dialog");                                   //  defog slider
   zdialog_add_widget(zd,"label","labdefog","hbdefog",TX("- fog/haze"),"space=8");
   zdialog_add_widget(zd,"hscale","minusfog","hbdefog","0|1|0.01|0","expand");

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

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

   zdialog_resize(zd,300,0);
   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;

   float       Fapply = 0;

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

   zdialog_fetch(zd,"minusfog",minusfog);
   zdialog_fetch(zd,"plusbrite",plusbrite);                                            //  get dialog inputs
   zdialog_fetch(zd,"minusblue",minusblue);

   if (strstr("minusfog plusbrite minusblue",event)) Fapply = 1;                       //  user input

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

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

   if (strmatch(event,"apply")) {                                                      //  from script
      Fapply = 1;
      zd->zstat = 2;
   }

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

   if (zd->zstat == 1)                                                                 //  [reset]
   {
      zd->zstat = 0;                                                                   //  keep dialog active
      zdialog_stuff(zd,"minusfog",0);
      zdialog_stuff(zd,"plusbrite",0);                                                 //  reset dialog inputs
      zdialog_stuff(zd,"minusblue",0);
      minusfog = plusbrite = minusblue = 0;
      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;
   }

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

   return 1;
}


//  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         Fend, px, py;
   float       blend;
   float       *pix1, *pix3;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       maxrgb, maxrdc, actrdc, maxadd, actadd, ratio;

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

      maxrgb = R1;
      if (G1 > maxrgb) maxrgb = G1;                                                    //  get max. RGB value
      if (B1 > maxrgb) maxrgb = B1;
      if (maxrgb < 2) continue;                                                        //  no fog by definition

      maxrdc = 255 - maxrgb;                                                           //  max. possible white reduction
      actrdc = maxrdc * minusfog;                                                      //  act. white reduction

      R9 = R1 - actrdc;                                                                //  remove white from RGB
      G9 = G1 - actrdc;
      B9 = B1 - actrdc;

      maxadd = maxrdc;                                                                 //  max. brightness to add
      actadd = maxadd * plusbrite;                                                     //  act. brightness add
      ratio = (maxrgb + actadd) / maxrgb;                                              //  new/old brightness ratio

      R9 = R9 * ratio;                                                                 //  add brightness to RGB
      G9 = G9 * ratio;
      B9 = B9 * ratio;

      B9 = B9 * (1.0 - minusblue);                                                     //  reduce blue

      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 = 
               TX("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 = TX("RED EYES");

   printf("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(TX("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,TX("%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 = TX("Drag mouse to select. Erase. Repeat. \n"
                                "Click: extend selection to mouse.");
   F1_help_topic = TX("SMART ERASE");

   printf("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(TX("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",TX("Radius"),"space=5");
   zdialog_add_widget(zd,"zspin","radius","hb2","1|30|1|10");
   zdialog_add_widget(zd,"label","labb","hb2",TX("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",TX("New Area"),"space=3");
   zdialog_add_widget(zd,"button","show","hb3",TX("Show"),"space=3");
   zdialog_add_widget(zd,"button","hide","hb3",TX("Hide"),"space=3");
   zdialog_add_widget(zd,"button","erase","hb3",TX("Erase"),"space=3");
   zdialog_add_widget(zd,"button","undo1","hb3",TX("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 = TX("left drag: reduce halo, right drag: undo");

   F1_help_topic = TX("REMOVE HALO");

   printf("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             |
            |                                            |
            |                                   [OK] [X] |
            |____________________________________________|

   ***/

   zdialog *zd = zdialog_new(TX("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",TX("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",TX("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",TX("bright halo"),"space=3");
   zdialog_add_widget(zd,"radio","dark","hb3",TX("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)
{
   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 = TX("JPEG ARTIFACTS");

   printf("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(TX("JPEG Artifacts"),Mwin,TX("Reset"),TX("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",TX("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",TX("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 = TX("ANTI-ALIAS");

   printf("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(TX("Anti-Alias"),Mwin,TX("Reset"),TX("Apply"),"OK","X",null);
   zdialog_add_widget(zd,"hbox","hbthresh","dialog",0,"space=10");
   zdialog_add_widget(zd,"label","labthresh","hbthresh",TX("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
      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 = TX("ADJUST RGB");

   printf("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 All    =====[]=====  |
         |  Contrast Red    =====[]=====  |
         |  Contrast Green  =====[]=====  |
         |  Contrast Blue   =====[]=====  |
         |                                |
         |               [Reset] [OK] [X] |
         |________________________________|

***/

   zdialog *zd = zdialog_new(TX("Adjust RGB"),Mwin,TX("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",TX("+Brightness"));
   zdialog_add_widget(zd,"label","labRedDens","vb1",TX("+Red -Cyan"));
   zdialog_add_widget(zd,"label","labGreenDens","vb1",TX("+Green -Magenta"));
   zdialog_add_widget(zd,"label","labBlueDens","vb1",TX("+Blue -Yellow"));
   zdialog_add_widget(zd,"hsep","sep1","vb1");
   zdialog_add_widget(zd,"label","labContrast","vb1",TX("Contrast All"));
   zdialog_add_widget(zd,"label","labRedCon","vb1",TX("Contrast Red"));
   zdialog_add_widget(zd,"label","labGreenCon","vb1",TX("Contrast Green"));
   zdialog_add_widget(zd,"label","labBlueCon","vb1",TX("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("%.3f %.3f %.3f %.3f %.3f %.3f %.3f %.3f",                       //  edit parameters --> edit history      25.1
                           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 = TX("ADJUST HSL");

   printf("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(TX("Adjust HSL"),Mwin,TX("Reset"),"OK","X",null);
   EF_HSL.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"label","labmatch","hb1",TX("Input color to match and adjust:"),"space=5");
   zdialog_add_widget(zd,"colorbutt","matchRGB","hb1","0|0|0");
   zdialog_add_ttip(zd,"matchRGB",TX("shift+click on image to select color"));

   zdialog_add_widget(zd,"hbox","hbmu","dialog");
   zdialog_add_widget(zd,"label","labmu","hbmu",TX("Match using:"),"space=5");
   zdialog_add_widget(zd,"check","Huse","hbmu",TX("Hue"),"space=3");
   zdialog_add_widget(zd,"check","Suse","hbmu",TX("Saturation"),"space=3");
   zdialog_add_widget(zd,"check","Luse","hbmu",TX("Lightness"),"space=3");

   zdialog_add_widget(zd,"hbox","hbmatch","dialog");
   zdialog_add_widget(zd,"label","labmatch","hbmatch",TX("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",TX("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",TX("Color Hue"));
   zdialog_add_widget(zd,"check","Sout","vb1",TX("Saturation"));
   zdialog_add_widget(zd,"check","Lout","vb1",TX("Lightness"));
   zdialog_add_widget(zd,"label","labadjust","vb1",TX("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;
}


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

//  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 = TX("REMOVE DUST");

   printf("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(TX("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",TX("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",TX("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",TX("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",TX("Erase"),"space=5");
   zdialog_add_widget(zd,"button","red","hbbutts",TX("Red"),"space=5");
   zdialog_add_widget(zd,"button","undo1","hbbutts",TX("Undo Last"),"space=5");
   zdialog_add_widget(zd,"button","apply","hbbutts",TX("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);

   zadd_locked(NFbusy,+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;
   zadd_locked(NFbusy,-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);

   F1_help_topic = TX("FRINGES");

   printf("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(TX("Color Fringes"),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",TX("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",TX("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",TX("Find optimum factors:"));
   zdialog_add_widget(zd,"button","search","hbopt",TX("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;

   zadd_locked(NFbusy,+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;
      }
   }

   zadd_locked(NFbusy,-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;
}


