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

   Fotoxx      edit photos and manage collections

   Copyright 2007-2016 Michael Cornelison
   Source URL: http://kornelix.net
   Contact: kornelix@posteo.de

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

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

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

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

   Fotoxx image edit - Repair menu functions

   m_sharpen               sharpen an image
   m_blur                  blur an image
   m_denoise               remove noise from an image
   m_smart_erase           replace pixels inside selected areas with background
   m_redeye                remove red-eyes from flash photos
   m_paint_clone           paint with an RGB color or with pixels from another area
   m_paint_transp          paint more or less transparency on an image
   m_add_transp            add transparency based on image attributes
   m_color_mode            convert between color, B&W, sepia, positive, negative
   m_shift_colors          gradually shift selected RGB colors into other colors
   m_colorsat              adjust color saturation
   m_adjust_RGB            adjust brightness/color using RGB or CMY colors
   m_adjust_HSL            adjust color using HSL model
   m_ramp_brightness       adjust brightness graduaally across the image
   m_ramp_color            shift RGB colors in selected image areas
   m_match_color           adjust image colors to match those in a chosen image
   m_color_profile         convert from one color profile to another
   m_remove_dust           remove dust specs from an image
   m_anti_alias            suppress pixel steps (jaggies) from image edges
   m_color_fringes         stretch/shrink RGB color planes to remove color fringes
   m_stuck_pixels          find and fix stuck pixels from camera sensor defects

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

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

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


//  image sharpen functions

namespace sharpen
{
   int      ww, hh;                                                              //  image dimensions
   int      UM_radius, UM_amount, UM_thresh;
   int      GR_amount, GR_thresh;
   int      KH_radius;
   int      MD_radius, MD_dark, MD_light, *MD_britemap;
   char     sharp_function[4] = "";

   int      brhood_radius;
   float    brhood_kernel[200][200];                                             //  up to radius = 99
   char     brhood_method;                                                       //  g = gaussian, f = flat distribution
   float    *brhood_brightness;                                                  //  neighborhood brightness per pixel

   VOL int  sharp_cancel, brhood_cancel;                                         //  avoid GCC optimizing code away

   editfunc    EFsharp;
}


//  menu function

void m_sharpen(GtkWidget *, cchar *menu)
{
   using namespace sharpen;

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

   F1_help_topic = "sharpen_image";

   EFsharp.menuname = menu;
   EFsharp.menufunc = m_sharpen;
   EFsharp.funcname = "sharpen";
   EFsharp.Farea = 2;                                                            //  select area usable
   EFsharp.threadfunc = sharp_thread;                                            //  thread function
   EFsharp.Frestart = 1;                                                         //  allow restart
   EFsharp.FusePL = 1;                                                           //  use with paint/lever edits OK
   EFsharp.Fscript = 1;                                                          //  scripting supported                15.10
   if (! edit_setup(EFsharp)) return;                                            //  setup edit

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

/***
          ___________________________________________
         |                 Sharpen                   |
         |                                           |
         |  [] unsharp mask         radius  [__|-+]  |
         |                          amount  [__|-+]  |
         |                        threshold [__|-+]  |
         |                                           |
         |  [] gradient             amount  [__|-+]  |
         |                        threshold [__|-+]  |
         |                                           |
         |  [] Kuwahara             radius  [__|-+]  |
         |                                           |
         |  [] median diff          radius  [__|-+]  |             16.04
         |                           dark   [__|-+]  |
         |                           light  [__|-+]  |
         |                                           |
         |          [reset] [apply] [done] [cancel]  |
         |___________________________________________|

***/

   zdialog *zd = zdialog_new(ZTX("Sharpen"),Mwin,Breset,Bapply,Bdone,Bcancel,null);
   EFsharp.zd = zd;

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

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

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

   zdialog_add_widget(zd,"hsep","sep5","dialog");                                //  median diff
   zdialog_add_widget(zd,"hbox","hb5","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb51","hb5",0,"space=2");
   zdialog_add_widget(zd,"label","space","hb5",0,"expand");
   zdialog_add_widget(zd,"vbox","vb52","hb5",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vb53","hb5",0,"homog|space=2");
   zdialog_add_widget(zd,"check","MD","vb51","median diff","space=5");
   zdialog_add_widget(zd,"label","lab51","vb52",Bradius);
   zdialog_add_widget(zd,"label","lab52","vb52",ZTX("dark"));
   zdialog_add_widget(zd,"label","lab53","vb52",ZTX("light"));
   zdialog_add_widget(zd,"spin","radiusMD","vb53","1|20|1|3");
   zdialog_add_widget(zd,"spin","darkMD","vb53","0|20|1|1");
   zdialog_add_widget(zd,"spin","lightMD","vb53","0|20|1|1");

   zdialog_restore_inputs(zd);
   
   zdialog_fetch(zd,"UM",ii);                                                    //  set function from checkboxes       15.10
   if (ii) strcpy(sharp_function,"UM");
   zdialog_fetch(zd,"GR",ii);
   if (ii) strcpy(sharp_function,"GR");
   zdialog_fetch(zd,"KH",ii);
   if (ii) strcpy(sharp_function,"KH");
   zdialog_fetch(zd,"MD",ii);
   if (ii) strcpy(sharp_function,"MD");

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

   return;
}


//  dialog event and completion callback function

int sharp_dialog_event(zdialog *zd, cchar *event)                                //  reworked for script files          15.10
{
   using namespace sharpen;
   
   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,"amountGR",GR_amount);
   zdialog_fetch(zd,"threshGR",GR_thresh);
   zdialog_fetch(zd,"radiusKH",KH_radius);
   zdialog_fetch(zd,"radiusMD",MD_radius);
   zdialog_fetch(zd,"darkMD",MD_dark);
   zdialog_fetch(zd,"lightMD",MD_light);

   if (strmatch(event,"apply")) zd->zstat = 2;                                   //  from script file                   15.10
   if (strmatch(event,"done")) zd->zstat = 3;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 3;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 4;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 4;                                  //  escape = cancel                    15.07
   
   sharp_cancel = 0;

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

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

      if (zd->zstat == 3) {
         edit_done(0);                                                           //  done
         return 1;
      }

      sharp_cancel = 1;                                                          //  cancel or [x]
      edit_cancel(0);                                                            //  discard edit
      return 1;
   }

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

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

   return 1;
}


//  sharpen image thread function

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

   int sharp_UM();
   int sharp_GR();
   int sharp_KH();
   int sharp_MD();

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

      if (strmatch(sharp_function,"UM")) sharp_UM();
      if (strmatch(sharp_function,"GR")) sharp_GR();
      if (strmatch(sharp_function,"KH")) sharp_KH();
      if (strmatch(sharp_function,"MD")) sharp_MD();

      CEF->Fmods++;
      CEF->Fsaved = 0;
      Fpaint2();
   }

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


//  image sharpen function using unsharp mask

int sharp_UM()
{
   using namespace sharpen;

   void  brhood_calc(int radius, char method);                                   //  compute neighborhood brightness
   void * sharp_UM_wthread(void *arg);

   int      ii, cc;

   cc = ww * hh * sizeof(float);
   brhood_brightness = (float *) zmalloc(cc);

   brhood_calc(UM_radius,'f');
   if (sharp_cancel) return 1;

   if (sa_stat == 3) Fbusy_goal = sa_Npixel;
   else  Fbusy_goal = ww * hh;
   Fbusy_done = 0;

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

   Fbusy_goal = 0;

   zfree(brhood_brightness);
   return 1;
}


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

   int         index = *((int *) arg);
   int         px, py, ii, dist = 0;
   float       amount, thresh, bright;
   float       mean, incr, ratio, f1, f2;
   float       red1, green1, blue1, red3, green3, blue3;
   float       *pix1, *pix3;

   for (py = index; py < hh; py += NWT)                                          //  loop all image3 pixels
   for (px = 0; px < ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      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 * ww + 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;

      red1 = pix1[0];                                                            //  input RGB
      green1 = pix1[1];
      blue1 = pix1[2];

      red3 = ratio * red1;                                                       //  output RGB
      if (red3 > 255) red3 = 255;
      green3 = ratio * green1;
      if (green3 > 255) green3 = 255;
      blue3 = ratio * blue1;
      if (blue3 > 255) blue3 = 255;

      if (sa_stat == 3 && dist < sa_blend) {                                     //  select area is active,
///      f1 = 1.0 * dist / sa_blend;                                             //    blend changes over sa_blend
         f1 = sa_blendfunc(dist);                                                //  16.08
         f2 = 1.0 - f1;
         red3 = f1 * red3 + f2 * red1;
         green3 = f1 * green3 + f2 * green1;
         blue3 = f1 * blue3 + f2 * blue1;
      }

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

      Fbusy_done++;                                                              //  track progress
      if (sharp_cancel) break;
   }

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


//  sharpen image by increasing brightness gradient

int sharp_GR()
{
   using namespace sharpen;

   void * sharp_GR_wthread(void *arg);

   if (sa_stat == 3) Fbusy_goal = sa_Npixel;
   else  Fbusy_goal = ww * hh;
   Fbusy_done = 0;

   for (int ii = 0; ii < NWT; ii++)                                              //  start worker threads
      start_wthread(sharp_GR_wthread,&Nval[ii]);
   wait_wthreads();                                                              //  wait for completion

   Fbusy_goal = 0;
   return 1;
}


void * sharp_GR_wthread(void *arg)                                               //  worker thread function 
{
   using namespace sharpen;

   float       *pix1, *pix3;
   int         ii, px, py, dist = 0;
   int         nc = E1pxm->nc;                                                   //  15.09
   float       amount, thresh;
   float       b1, b1x, b1y, b3x, b3y, b3, bf, f1, f2;
   float       red1, green1, blue1, red3, green3, blue3;

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

   amount = 1 + 0.01 * GR_amount;                                                //  1.0 - 5.0
   thresh = GR_thresh;                                                           //  0 - 100

   for (py = index + 1; py < hh; py += NWT)                                      //  loop all image pixels
   for (px = 1; px < ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel is outside area
      }

      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         15.09
      b1y = b1 - pixbright(pix1-nc * ww);                                        //  vertical
      f1 = fabsf(b1x + 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 * ww) + 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;

      red1 = pix1[0];                                                            //  input RGB
      green1 = pix1[1];
      blue1 = pix1[2];

      red3 = bf * red1;                                                          //  output RGB
      if (red3 > 255.9) red3 = 255.9;
      green3 = bf * green1;
      if (green3 > 255.9) green3 = 255.9;
      blue3 = bf * blue1;
      if (blue3 > 255.9) blue3 = 255.9;

      if (sa_stat == 3 && dist < sa_blend) {                                     //  select area is active,
///      f1 = 1.0 * dist / sa_blend;                                             //    blend changes over sa_blend
         f1 = sa_blendfunc(dist);                                                //  16.08
         f2 = 1.0 - f1;
         red3 = f1 * red3 + f2 * red1;
         green3 = f1 * green3 + f2 * green1;
         blue3 = f1 * blue3 + f2 * blue1;
      }

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

      Fbusy_done++;                                                              //  track progress
      if (sharp_cancel) break;
   }

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


//  sharpen edges using the Kuwahara algorithm

int sharp_KH()
{
   using namespace sharpen;

   void * sharp_KH_wthread(void *arg);
   
   if (sa_stat == 3) Fbusy_goal = sa_Npixel;
   else  Fbusy_goal = ww * hh;
   Fbusy_done = 0;

   for (int ii = 0; ii < NWT; ii++)                                              //  start worker threads
      start_wthread(sharp_KH_wthread,&Nval[ii]);
   wait_wthreads();                                                              //  wait for completion

   Fbusy_goal = 0;
   return 1;
}


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

   float       *pix1, *pix3;
   int         px, py, qx, qy, rx, ry;
   int         ii, rad, N, dist = 0;
   float       red, green, blue, red2, green2, blue2;
   float       vmin, vall, vred, vgreen, vblue;
   float       red3, green3, blue3;
   float       f1, f2;

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

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

   for (py = index + rad; py < hh-rad; py += NWT)                                //  loop all image pixels
   for (px = rad; px < ww-rad; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel is outside area
      }

      vmin = 99999;
      red3 = green3 = blue3 = 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;
            red3 = red;
            green3 = green;
            blue3 = blue;
         }
      }

      if (sa_stat == 3 && dist < sa_blend) {                                     //  if select area is active,
///      f1 = 1.0 * dist / sa_blend;                                             //    blend changes over sa_blend
         f1 = sa_blendfunc(dist);                                                //  16.08
         f2 = 1.0 - f1;
         pix1 = PXMpix(E1pxm,px,py);                                             //  input pixel
         red3 = f1 * red3 + f2 * pix1[0];
         green3 = f1 * green3 + f2 * pix1[1];
         blue3 = f1 * blue3 + f2 * pix1[2];
      }

      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;

      Fbusy_done++;                                                              //  track progress
      if (sharp_cancel) break;
   }

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


//  sharpen edges using the median difference algorithm

int sharp_MD()                                                                   //  16.04
{
   using namespace sharpen;

   void * sharp_MD_wthread(void *arg);
   
   int      px, py, ii;
   float    *pix1;
   
   MD_britemap = (int *) zmalloc(ww * hh * sizeof(int));
   
   for (py = 0; py < hh; py++)                                                   //  loop all pixels
   for (px = 0; px < ww; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);                                                //  initz. pixel brightness map
      ii = py * ww + px;
      MD_britemap[ii] = pix1[0] + pix1[1] + pix1[2];
   }

   if (sa_stat == 3) Fbusy_goal = sa_Npixel;
   else  Fbusy_goal = ww * hh;
   Fbusy_done = 0;

   for (int ii = 0; ii < NWT; ii++)                                              //  start worker threads
      start_wthread(sharp_MD_wthread,&Nval[ii]);
   wait_wthreads();                                                              //  wait for completion

   Fbusy_goal = 0;

   zfree(MD_britemap);
   return 1;
}


void * sharp_MD_wthread(void *arg)                                               //  worker thread function
{
   using namespace sharpen;
   
   int         index = *((int *) arg);
   int         rad, dark, light, *britemap;
   int         ii, px, py, dist = 0;
   int         dy, dx, ns;
   float       R, G, B, R2, G2, B2;
   float       F, f1, f2;
   float       *pix1, *pix3;
   int         bright, median;
   int         bsortN[1681];                                                     //  radius <= 20 (41 x 41 pixels)
   
   rad = MD_radius;                                                              //  parameters from dialog
   dark = MD_dark;
   light = MD_light;
   britemap = MD_britemap;

   for (py = index+rad; py < hh-rad; py += NWT)                                  //  loop all image3 pixels
   for (px = rad; px < ww-rad; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

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

      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 * ww + dx;
         bsortN[ns] = britemap[ii];
         ns++;
      }

      HeapSort(bsortN,ns);                                                       //  sort the pixels
      median = bsortN[ns/2];                                                     //  median brightness
      
      R = pix3[0];
      G = pix3[1];
      B = pix3[2];
      
      bright = R + G + B;
      
      if (bright < median) {
         F = 1.0 - 0.1 * dark * (median - bright) / (median + 100);              //  0.91 ... 1.0
         R2 = R * F;
         G2 = G * F;
         B2 = B * F;
         if (R2 > 0 && G2 > 0 && B2 > 0) {
            R = R2;
            G = G2;
            B = B2;
         }
      }
      
      if (bright > median) {
         F = 1.0 + 0.03 * light * (bright - median) / (median + 100);            //  1.0 ... 1.026
         R2 = R * F;
         G2 = G * F;
         B2 = B * F;
         if (R2 < 255 && G2 < 255 && B2 < 255) {
            R = R2;
            G = G2;
            B = B2;
         }
      }

      if (sa_stat == 3 && dist < sa_blend) {                                     //  select area is active,
///      f1 = 1.0 * dist / sa_blend;                                             //    blend changes over sa_blend
         f1 = sa_blendfunc(dist);                                                //  16.08
         f2 = 1.0 - f1;
         R = f1 * R + f2 * pix1[0];
         G = f1 * G + f2 * pix1[1];
         B = f1 * B + f2 * pix1[2];
      }
      
      pix3[0] = R;
      pix3[1] = G;
      pix3[2] = B;

      Fbusy_done++;                                                              //  track progress
   }

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


//  Compute the mean brightness of all pixel neighborhoods,
//  using a Guassian 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 Guassian method.

void brhood_calc(int radius, char method)
{
   using namespace sharpen;

   void * brhood_wthread(void *arg);

   int      rad, radflat2, dx, dy, ii;
   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 == 3) Fbusy_goal = sa_Npixel;                                     //  set up progress tracking
   else Fbusy_goal = ww * hh;
   Fbusy_done = 0;

   for (ii = 0; ii < NWT; ii++)                                                  //  start worker threads
      start_wthread(brhood_wthread,&Nval[ii]);
   wait_wthreads();                                                              //  wait for completion

   Fbusy_goal = 0;
   return;
}


//  worker thread function

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

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

   if (brhood_method == 'g')                                                     //  use round gaussian distribution
   {
      for (py = index; py < hh; py += NWT)
      for (px = 0; px < ww; px++)
      {
         if (sa_stat == 3 && sa_mode != mode_image) {                            //  select area, not whole image
            ii = py * ww + 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 > hh-1) continue;
            if (qx < 0 || qx > ww-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 * ww + px;
         brhood_brightness[ii] = bmean;                                          //  pixel value

         Fbusy_done++;                                                           //  track progress
         if (sharp_cancel) break;
      }
   }

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

      for (py = index; py < hh; py += NWT)
      for (px = 0; px < ww; px++)
      {
         if (sa_stat == 3 && sa_mode != mode_image) {                            //  select area, not whole image
            ii = py * ww + 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 > hh-1) continue;
               if (qx < 0 || qx > ww-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 > hh-1) continue;
                  pixel = PXMpix(E1pxm,qx,qy);
                  bsum -= pixbright(pixel);
                  bsamp -= 1;
               }
            }
            qx = px+rad;                                                         //  add last column
            if (qx < ww) {
               for (qy = py-rad; qy <= py+rad; qy++)
               {
                  if (qy < 0 || qy > hh-1) continue;
                  pixel = PXMpix(E1pxm,qx,qy);
                  bsum += pixbright(pixel);
                  bsamp += 1;
               }
            }
         }

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

         Fbusy_done++;                                                           //  track progress
         if (sharp_cancel) break;
      }
   }

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


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

//  image blur function

namespace blur_names
{
   float       blur_radius;
   float       blur_weight[202][202];                                            //  up to blur radius = 200
   int         blur_Npixels;
   char        *blur_flags;
   VOL int     blur_cancel;                                                      //  GCC inconsistent
   editfunc    EFblur;
}


//  menu function

void m_blur(GtkWidget *, cchar *menu)
{
   using namespace blur_names;

   int    blur_dialog_event(zdialog *zd, cchar *event);
   void * blur_thread(void *);
   int    cc;

   F1_help_topic = "blur_image";

   EFblur.menuname = menu;
   EFblur.menufunc = m_blur;
   EFblur.funcname = "blur";
   EFblur.Farea = 2;                                                             //  select area usable
   EFblur.threadfunc = blur_thread;                                              //  thread function
   EFblur.Frestart = 1;                                                          //  allow restart
   EFblur.Fscript = 1;                                                           //  scripting supported                15.10
   if (! edit_setup(EFblur)) return;                                             //  setup edit

   blur_radius = 0.5;
   blur_cancel = 0;

   cc = E3pxm->ww * E3pxm->hh;
   blur_flags = (char *) zmalloc(cc);                                            //  allocate pixel flags

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

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

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


//  dialog event and completion callback function

int blur_dialog_event(zdialog * zd, cchar *event)
{
   using namespace blur_names;

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   
   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(0);                                          //  commit edit
      else {                                                                     //  cancel
         blur_cancel = 1;
         edit_cancel(0);                                                         //  discard edit
      }
      zfree(blur_flags);
      return 1;
   }

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

   return 1;
}


//  image blur thread function

void * blur_thread(void *)
{
   using namespace blur_names;

   void * blur_wthread(void *arg);

   int         dx, dy, cc;
   float       rad, radflat2;
   float       m, d, w, sum;

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

      rad = blur_radius - 0.2;
      radflat2 = rad * rad;

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

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

      m = sqrt(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;

      if (sa_stat == 3) Fbusy_goal = sa_Npixel;
      else  Fbusy_goal = E3pxm->ww * E3pxm->hh;
      Fbusy_done = 0;

      cc = E3pxm->ww * E3pxm->hh;                                                //  clear flags
      memset(blur_flags,0,cc);

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

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

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


void * blur_wthread(void *arg)                                                   //  worker thread function
{
   using namespace blur_names;

   int      index = *((int *) arg);
   int      px, py, ii, dist = 0;
   int      dx, dy, adx, ady, rad;
   int      ww = E3pxm->ww, hh = E3pxm->hh;
   int      Npix;
   int      nc = E1pxm->nc;                                                      //  15.09
   float    red, green, blue;
   float    weight1, weight2, f1, f2;
   float    *pix1, *pix3, *pixN;

   for (py = index; py < hh-1; py += NWT)                                        //  loop all image pixels
   for (px = 0; px < ww-1; px++)
   {
      if (blur_cancel) exit_wthread();                                           //  user cancel

      ii = py * ww + px;
      if (blur_flags[ii]) continue;                                              //  pixel already done, skip
      blur_flags[ii] = 1;                                                        //  this one is mine

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

      pix1 = PXMpix(E1pxm,px,py);                                                //  source pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  target pixel
      
      rad = blur_radius;
      red = green = blue = 0;
      weight2 = 0.0;
      Npix = 0;                                                                  //  pixels sampled

      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 > ww-1) continue;                                //  omit pixels off edge
         if (py+dy < 0 || py+dy > hh-1) continue;

         if (sa_stat == 3) {
            ii = (py+dy) * ww + px+dx;
            dist = sa_pixmap[ii];
            if (! dist) continue;                                                //  omit pixels outside area
         }

         adx = abs(dx);
         ady = abs(dy);
         pixN = pix1 + (dy * ww + dx) * nc;                                      //  15.09
         weight1 = blur_weight[adx][ady];                                        //  weight at distance (dx,dy)
         weight2 += weight1;
         red += pixN[0] * weight1;                                               //  accumulate contributions
         green += pixN[1] * weight1;
         blue += pixN[2] * weight1;
         Npix++;                                                                 //  count pixels sampled
      }

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

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

      Fbusy_done++;                                                              //  track progress

      if (Npix < 2000) continue;                                                 //  mixed pixels are few, no shortcut

      rad = sqrt(Npix/2000);                                                     //  2000->1  8000->2  18000->3  32000->4 ...

      for (dy = -rad; dy <= +rad; dy++)                                          //  loop pixels within rad
      for (dx = -rad; dx <= +rad; dx++)
      {
         if (px+dx < 0 || px+dx > ww-1) continue;                                //  omit pixels off edge
         if (py+dy < 0 || py+dy > hh-1) continue;

         ii = (py+dy) * ww + px+dx;
         if (blur_flags[ii]) continue;                                           //  pixel already done, skip
         blur_flags[ii] = 1;                                                     //  this one is mine

         if (sa_stat == 3) {
            dist = sa_pixmap[ii];
            if (! dist) continue;                                                //  omit pixels outside area
         }

         pix3 = PXMpix(E3pxm,px+dx,py+dy);                                       //  set neighbors to same RGB values
         pix3[0] = red;
         pix3[1] = green;
         pix3[2] = blue;

         Fbusy_done++;                                                           //  track progress
      }
   }

   if (sa_stat == 3 && sa_blend > 0)                                             //  select area has edge blend
   {
      for (py = index; py < hh-1; py += NWT)                                     //  loop all image pixels
      for (px = 0; px < ww-1; px++)
      {
         ii = py * ww + px;
         dist = sa_pixmap[ii];
         if (! dist) continue;                                                   //  omit pixels outside area
         if (dist >= sa_blend) continue;                                         //  omit if > blendwidth from edge

         pix1 = PXMpix(E1pxm,px,py);                                             //  source pixel
         pix3 = PXMpix(E3pxm,px,py);                                             //  target pixel
///      f1 = 1.0 * dist / sa_blend;                                             //  blend changes over sa_blend
         f1 = sa_blendfunc(dist);                                                //  16.08
         f2 = 1.0 - f1;
         pix3[0] = f1 * pix3[0] + f2 * pix1[0];
         pix3[1] = f1 * pix3[1] + f2 * pix1[1];
         pix3[2] = f1 * pix3[2] + f2 * pix1[2];
      }
   }

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


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

//  image noise reduction

namespace denoise
{
   enum     denoise_method 
            { flatten, median, tophat, wavelets }
            denoise_method;

   int      denoise_radius;
   float    denoise_thresh;
   float    denoise_darkareas;

   zdialog  *zd_denoise_measure;
   cchar    *mformat = "  mean RGB:  %5.0f %5.0f %5.0f ";
   cchar    *nformat = " mean noise: %5.2f %5.2f %5.2f ";
   
   int      ww, hh;                                                              //  image dimensions
   int8     *Fpix, *Gpix;

   editfunc    EFdenoise;
}


//  menu function

void m_denoise(GtkWidget *, cchar *menu)
{
   using namespace denoise;

   int    denoise_dialog_event(zdialog *zd, cchar *event);
   void * denoise_thread(void *);
   
   cchar  *tip = ZTX("Apply repeatedly while watching the image.");
   cchar  *Bmeasure = ZTX("Measure");

   F1_help_topic = "denoise";

   EFdenoise.menuname = menu;
   EFdenoise.menufunc = m_denoise;
   EFdenoise.funcname = "denoise";
   EFdenoise.Farea = 2;                                                          //  select area usable
   EFdenoise.threadfunc = denoise_thread;                                        //  thread function
   EFdenoise.Frestart = 1;                                                       //  allow restart
   EFdenoise.FusePL = 1;                                                         //  use with paint/lever edits OK
   EFdenoise.Fscript = 1;                                                        //  scripting supported                15.10
   if (! edit_setup(EFdenoise)) return;                                          //  setup edit

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

/***
          _____________________________________________
         |           Noise Reduction                   |
         |  Apply repeatedly while watching the image. |
         |                                             |
         |  (o)  Flatten              Radius [___+|-]  |
         |  (o)  Median               Radius [___+|-]  |
         |  (o)  Top Hat              Radius [___+|-]  |
         |  (o)  Wavelets          Threshold [___+|-]  |
         |                                             |
         | dark areas =========[]=========== all areas |
         |                                             |
         |   [Measure] [Apply] [Reset] [Done] [Cancel] |
         |_____________________________________________|

***/

   zdialog *zd = zdialog_new(ZTX("Noise Reduction"),Mwin,Bmeasure,Bapply,Breset,Bdone,Bcancel,null);
   EFdenoise.zd = zd;
   
   zdialog_add_widget(zd,"label","labtip","dialog",tip,"space=3");

   zdialog_add_widget(zd,"hbox","hbvb","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","space","hbvb",0,"space=3");
   zdialog_add_widget(zd,"vbox","vb1","hbvb",0,"homog|space=3");
   zdialog_add_widget(zd,"vbox","vb2","hbvb",0,"homog|space=3");
   zdialog_add_widget(zd,"label","space","hbvb",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb3","hbvb",0,"homog|space=3");
   zdialog_add_widget(zd,"vbox","vb4","hbvb",0,"homog|space=3");
   zdialog_add_widget(zd,"label","space","hbvb",0,"space=2");

   zdialog_add_widget(zd,"radio","flatten","vb1");
   zdialog_add_widget(zd,"radio","median","vb1");
   zdialog_add_widget(zd,"radio","tophat","vb1");
   zdialog_add_widget(zd,"radio","wavelets","vb1");

   zdialog_add_widget(zd,"label","labflatten","vb2",ZTX("Flatten"));
   zdialog_add_widget(zd,"label","labmedian","vb2",ZTX("Median"));
   zdialog_add_widget(zd,"label","labtophat","vb2","Top Hat");
   zdialog_add_widget(zd,"label","labwavelets","vb2","Wavelets");

   zdialog_add_widget(zd,"label","labradflatten","vb3",Bradius);
   zdialog_add_widget(zd,"label","labradmedian","vb3",Bradius);
   zdialog_add_widget(zd,"label","labradtophat","vb3",Bradius);
   zdialog_add_widget(zd,"label","labwaveletsthresh","vb3",Bthresh);

   zdialog_add_widget(zd,"spin","radflatten","vb4","1|9|1|3");
   zdialog_add_widget(zd,"spin","radmedian","vb4","1|9|1|2");
   zdialog_add_widget(zd,"spin","radtophat","vb4","1|9|1|2");
   zdialog_add_widget(zd,"spin","waveletsthresh","vb4","0.0|8|0.1|1");

   zdialog_add_widget(zd,"hbox","hbdark","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labdark","hbdark",ZTX("dark areas"),"space=8");
   zdialog_add_widget(zd,"hscale","darkareas","hbdark","0|200|1|200","expand");
   zdialog_add_widget(zd,"label","laball","hbdark",ZTX("all areas"),"space=8");

   zdialog_restore_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, cchar *event)                             //  reworked for script files          15.10
{
   using namespace denoise;

   void   denoise_measure();

   int    ii;

   wait_thread_idle();                                                           //  bugfix                             16.07
   
   if (strmatch(event,"apply")) zd->zstat = 2;                                   //  from script file
   if (strmatch(event,"done")) zd->zstat = 4;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 4;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 5;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 5;                                  //  escape = cancel                    15.07

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

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

      if (zd->zstat == 4) {                                                      //  [done]  commit edit
         edit_done(0);
         return 1;
      }
      
      edit_cancel(0);                                                            //  [cancel] or [x]  discard edit

      if (zd_denoise_measure) {
         freeMouse();
         zdialog_free(zd_denoise_measure);
         zd_denoise_measure = 0;
      }

      return 1;
   }
   
   zdialog_fetch(zd,"darkareas",denoise_darkareas);
   
   if (strmatch(event,"blendwidth")) signal_thread();
   
   if (strstr("flatten median tophat wavelets",event)) {                         //  checkboxes work like radio buttons
      zdialog_stuff(zd,"flatten",0);
      zdialog_stuff(zd,"median",0);
      zdialog_stuff(zd,"tophat",0);
      zdialog_stuff(zd,"wavelets",0);
      zdialog_stuff(zd,event,1);
   }

   zdialog_fetch(zd,"flatten",ii);
   if (ii) {
      denoise_method = flatten;
      zdialog_fetch(zd,"radflatten",denoise_radius);
   }

   zdialog_fetch(zd,"median",ii);
   if (ii) {
      denoise_method = median;
      zdialog_fetch(zd,"radmedian",denoise_radius);
   }
   
   zdialog_fetch(zd,"tophat",ii);
   if (ii) {
      denoise_method = tophat;
      zdialog_fetch(zd,"radtophat",denoise_radius);
   }
   
   zdialog_fetch(zd,"wavelets",ii);
   if (ii) {
      denoise_method = wavelets;
      zdialog_fetch(zd,"waveletsthresh",denoise_thresh);
   }
   
   return 1;
}


//  image noise reduction thread

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

   void * denoise_wthread(void *arg);
   void * denoise_wavelet_wthread(void *arg);

   int      ii, px, py, dist = 0;
   float    *pix3, *pix9;
   int      nc = E3pxm->nc, pcc = nc * sizeof(float);                            //  15.09

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

      E9pxm = PXM_copy(E3pxm);                                                   //  image3 is source, image9 is modified

      if (denoise_method == wavelets)                                            //  wavelet method
      {
         for (ii = 0; ii < 3; ii++)                                              //  start worker threads for wavelet denoise
            start_wthread(denoise_wavelet_wthread,&Nval[ii]);                    //  (one thread per RGB color)
         wait_wthreads();                                                        //  wait for completion
      }
      
      else                                                                       //  other method
      {
         if (sa_stat == 3) Fbusy_goal = sa_Npixel;
         else  Fbusy_goal = ww * hh;
         Fbusy_done = 0;

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

         Fbusy_goal = 0;
      }

      if (denoise_darkareas < 200)                                               //  if brightness threshhold set,      15.01
      {                                                                          //    revert brighter areas
         for (py = 0; py < hh; py++)
         for (px = 0; px < ww; px++)
         {
            if (sa_stat == 3) {                                                  //  select area active
               ii = py * ww + px;
               dist = sa_pixmap[ii];                                             //  distance from edge
               if (! dist) continue;                                             //  outside pixel
            }

            pix3 = PXMpix(E3pxm,px,py);                                          //  source pixel
            pix9 = PXMpix(E9pxm,px,py);                                          //  target pixel

            if (pix3[0] + pix3[1] + pix3[2] > 3 * denoise_darkareas)             //  revert brighter pixels
               memcpy(pix9,pix3,pcc);
         }
      }

      paintlock(1);                                                              //  block window updates               16.02
      PXM_free(E3pxm);                                                           //  image9 >> image3
      E3pxm = E9pxm;
      E9pxm = 0;
      paintlock(0);                                                              //  unblock window updates             16.02

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

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


void * denoise_wthread(void *arg)                                                //  worker thread for methods 1-5
{
   using namespace denoise;

   void  denoise_flatten(float *pix3, float *pix9);
   void  denoise_median(float *pix3, float *pix9);
   void  denoise_tophat(float *pix3, float *pix9);

   int         index = *((int *) arg);
   int         ii, px, py, rad, dist = 0;
   float       f1, f2;
   float       *pix1, *pix3, *pix9;

   rad = denoise_radius;

   for (py = index+rad; py < hh-rad; py += NWT)                                  //  loop all image3 pixels
   for (px = rad; px < ww-rad; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      pix3 = PXMpix(E3pxm,px,py);                                                //  source pixel
      pix9 = PXMpix(E9pxm,px,py);                                                //  target pixel

      if (denoise_method == flatten) denoise_flatten(pix3,pix9);
      if (denoise_method == median) denoise_median(pix3,pix9);
      if (denoise_method == tophat) denoise_tophat(pix3,pix9);

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

      Fbusy_done++;                                                              //  track progress
   }

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


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

//  Flatten: 
//  Flatten outlyer pixels within neighborhood group.
//  An outlier pixel has an RGB value outside one sigma of
//  the mean for all pixels within a given radius.

void denoise_flatten(float *pix3, float *pix9)
{
   using namespace denoise;

   int         rgb, dy, dx, rad, nn;
   float       nn1, val, sum, sum2, mean, variance, sigma;
   float       *pixN;
   int         nc = E3pxm->nc;                                                   //  15.09

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

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

      for (dy = -rad; dy <= rad; dy++)                                           //  loop surrounding pixels
      for (dx = -rad; dx <= rad; dx++)
      {
         if (dy == 0 && dx == 0) continue;                                       //  skip self
         pixN = pix3 + (dy * ww + dx) * nc;                                      //  15.09
         val = pixN[rgb];
         sum += val;
         sum2 += val * val;
      }

      mean = nn1 * sum;
      variance = nn1 * (sum2 - 2.0 * mean * sum) + mean * mean;
      sigma = sqrtf(variance);

      if (pix3[rgb] > mean + sigma)                                              //  if | pixel - mean | > sigma        16.07
         pix9[rgb] = mean + 0.8 * sigma;                                         //    flatten pixel
      else if (pix3[rgb] < mean - sigma) 
         pix9[rgb] = mean - 0.8 * sigma;
   }

   return;
}


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

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

void denoise_median(float *pix3, float *pix9)
{
   using namespace denoise;

   int         dy, dx, rad, ns, rgb;
   float       bsortN[400], *pixN;
   int         nc = E3pxm->nc;                                                   //  15.09

   rad = denoise_radius;

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

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

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

   return;
}


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

//  Top Hat:
//  Execute with increasing radius from 1 to limit.
//  Detect outlier by comparing with pixels along outer radius only.

void denoise_tophat(float *pix3, float *pix9)
{
   using namespace denoise;

   int         dy, dx, rad;
   float       minR, minG, minB, maxR, maxG, maxB;
   float       *pixN;
   int         nc = E3pxm->nc;                                                   //  15.09

   for (rad = 1; rad <= denoise_radius; rad++)
   for (int loops = 0; loops < 2; loops++)
   {
      minR = minG = minB = 255;
      maxR = maxG = maxB = 0;

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

         pixN = pix3 + (dy * ww + dx) * nc;                                      //  15.09
         if (pixN[0] < minR) minR = pixN[0];                                     //  find min and max per color
         if (pixN[0] > maxR) maxR = pixN[0];                                     //    among outermost pixels
         if (pixN[1] < minG) minG = pixN[1];
         if (pixN[1] > maxG) maxG = pixN[1];
         if (pixN[2] < minB) minB = pixN[2];
         if (pixN[2] > maxB) maxB = pixN[2];
      }

      if (pix3[0] < minR && pix9[0] < 254) pix9[0] += 2;                         //  if central pixel is outlier,       16.07
      if (pix3[0] > maxR && pix9[0] > 2)   pix9[0] -= 2;                         //    moderate its values
      if (pix3[1] < minG && pix9[1] < 254) pix9[1] += 2;
      if (pix3[1] > maxG && pix9[1] > 2)   pix9[1] -= 2;
      if (pix3[2] < minB && pix9[2] < 254) pix9[2] += 2;
      if (pix3[2] > maxB && pix9[2] > 2)   pix9[2] -= 2;
   }

   return;
}


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

//  Wavelet:
//  worker thread for wavelets method
//  do wavelet denoise for one color in each of 3 threads

void * denoise_wavelet_wthread(void *arg)
{
   using namespace denoise;

   void denoise_wavelet(float *fimg[3], uint ww2, uint hh2, float);

   int      rgb = *((int *) arg);                                                //  rgb color 0/1/2
   int      ii, jj;
   float    *fimg[3];
   float    f256 = 1.0 / 256.0;
   int      nc = E3pxm->nc;                                                      //  15.09

   if (sa_stat == 3) goto denoise_area;                                          //  select area is active

   fimg[0] = (float *) zmalloc(ww * hh * sizeof(float));
   fimg[1] = (float *) zmalloc(ww * hh * sizeof(float));
   fimg[2] = (float *) zmalloc(ww * hh * sizeof(float));

   for (ii = 0; ii < ww * hh; ii++)                                              //  extract one noisy color from E3
      fimg[0][ii] = E3pxm->pixels[nc*ii+rgb] * f256;                             //  15.09

   denoise_wavelet(fimg,ww,hh,denoise_thresh);

   for (ii = 0; ii < ww * hh; ii++)                                              //  save one denoised color to E9
      E9pxm->pixels[nc*ii+rgb] = 256.0 * fimg[0][ii];

   zfree(fimg[0]);
   zfree(fimg[1]);
   zfree(fimg[2]);

   exit_wthread();
   return 0;

denoise_area:

   int      px, py, pxl, pxh, pyl, pyh, ww2, hh2, dist;
   float    f1, f2;

   pxl = sa_minx - 16;
   if (pxl < 0) pxl = 0;
   pxh = sa_maxx + 16;
   if (pxh > ww) pxh = ww;

   pyl = sa_miny - 16;
   if (pyl < 0) pyl = 0;
   pyh = sa_maxy + 16;
   if (pyh > hh) pyh = hh;

   ww2 = pxh - pxl;
   hh2 = pyh - pyl;

   fimg[0] = (float *) zmalloc(ww2 * hh2 * sizeof(float));
   fimg[1] = (float *) zmalloc(ww2 * hh2 * sizeof(float));
   fimg[2] = (float *) zmalloc(ww2 * hh2 * sizeof(float));

   for (py = 0; py < hh2; py++)
   for (px = 0; px < ww2; px++)
   {
      ii = py * ww2 + px;
      jj = (py + pyl) * ww + (px + pxl);
      fimg[0][ii] = E3pxm->pixels[nc*jj+rgb] * f256;                             //  15.09
   }

   denoise_wavelet(fimg,ww2,hh2,denoise_thresh);

   for (py = 0; py < hh2; py++)
   for (px = 0; px < ww2; px++)
   {
      ii = py * ww2 + px;
      jj = (py + pyl) * ww + (px + pxl);

      dist = sa_pixmap[jj];
      if (! dist) continue;

      if (dist < sa_blend) {
///      f1 = 1.0 * dist / sa_blend;
         f1 = sa_blendfunc(dist);                                                //  16.08
         f2 = 1.0 - f1;
         E9pxm->pixels[nc*jj+rgb] = f1 * 256.0 * fimg[0][ii] + f2 * E3pxm->pixels[nc*jj+rgb];
      }
      else E9pxm->pixels[nc*jj+rgb] = 256.0 * fimg[0][ii];
   }

   zfree(fimg[0]);
   zfree(fimg[1]);
   zfree(fimg[2]);

   exit_wthread();
   return 0;
}


//  wavelet denoise algorithm
//  Adapted from Gimp wavelet plugin (and ultimately DCraw by Dave Coffin).
//  fimg[0][rows][cols] = one color of image to denoise
//  fimg[1] and [2] = working space
//  thresh (0-10) is the adjustable parameter

void denoise_wavelet(float *fimg[3], uint ww2, uint hh2, float thresh)
{
   void denoise_wavelet_avgpix(float *temp, float *fimg, int st, int size, int sc);

   float    *temp, thold, stdev[5];
   uint     ii, lev, lpass, hpass, size, col, row;
   uint     samples[5];

   size = ww2 * hh2;
   temp = (float *) zmalloc ((ww2 + hh2) * sizeof(float));
   hpass = 0;

   for (lev = 0; lev < 5; lev++)
   {
      lpass = ((lev & 1) + 1);                                                   //  1, 2, 1, 2, 1

      for (row = 0; row < hh2; row++)                                            //  average row pixels
      {
         denoise_wavelet_avgpix(temp, fimg[hpass] + row * ww2, 1, ww2, 1 << lev);

         for (col = 0; col < ww2; col++)
            fimg[lpass][row * ww2 + col] = temp[col];
      }

      for (col = 0; col < ww2; col++)                                            //  average column pixels
      {
         denoise_wavelet_avgpix(temp, fimg[lpass] + col, ww2, hh2, 1 << lev);

         for (row = 0; row < hh2; row++)
            fimg[lpass][row * ww2 + col] = temp[row];
      }

      thold = 5.0 / (1 << 6) * exp (-2.6 * sqrt (lev + 1)) * 0.8002 / exp (-2.6);

      stdev[0] = stdev[1] = stdev[2] = stdev[3] = stdev[4] = 0.0;
      samples[0] = samples[1] = samples[2] = samples[3] = samples[4] = 0;

      for (ii = 0; ii < size; ii++)
      {
         fimg[hpass][ii] -= fimg[lpass][ii];

         if (fimg[hpass][ii] < thold && fimg[hpass][ii] > -thold)
         {
            if (fimg[lpass][ii] > 0.8) {
               stdev[4] += fimg[hpass][ii] * fimg[hpass][ii];
               samples[4]++;
            }
            else if (fimg[lpass][ii] > 0.6) {
               stdev[3] += fimg[hpass][ii] * fimg[hpass][ii];
               samples[3]++;
            }
            else if (fimg[lpass][ii] > 0.4) {
               stdev[2] += fimg[hpass][ii] * fimg[hpass][ii];
               samples[2]++;
            }
            else if (fimg[lpass][ii] > 0.2) {
               stdev[1] += fimg[hpass][ii] * fimg[hpass][ii];
               samples[1]++;
            }
            else {
               stdev[0] += fimg[hpass][ii] * fimg[hpass][ii];
               samples[0]++;
            }
         }
      }

      stdev[0] = sqrt (stdev[0] / (samples[0] + 1));
      stdev[1] = sqrt (stdev[1] / (samples[1] + 1));
      stdev[2] = sqrt (stdev[2] / (samples[2] + 1));
      stdev[3] = sqrt (stdev[3] / (samples[3] + 1));
      stdev[4] = sqrt (stdev[4] / (samples[4] + 1));

      for (ii = 0; ii < size; ii++)                                              //  do thresholding
      {
         if (fimg[lpass][ii] > 0.8)
            thold = thresh * stdev[4];
         else if (fimg[lpass][ii] > 0.6)
            thold = thresh * stdev[3];
         else if (fimg[lpass][ii] > 0.4)
            thold = thresh * stdev[2];
         else if (fimg[lpass][ii] > 0.2)
            thold = thresh * stdev[1];
         else
            thold = thresh * stdev[0];

         if (fimg[hpass][ii] < -thold)
            fimg[hpass][ii] += thold;
         else if (fimg[hpass][ii] > thold)
            fimg[hpass][ii] -= thold;
         else
            fimg[hpass][ii] = 0;

         if (hpass) fimg[0][ii] += fimg[hpass][ii];
      }

      hpass = lpass;
   }

   for (ii = 0; ii < size; ii++)
      fimg[0][ii] = fimg[0][ii] + fimg[lpass][ii];

   zfree(temp);
   return;
}


//  average pixels in one column or row
//  st = row stride (row length) or column stride (1)
//  sc = 1, 2, 4, 8, 16 = pixels +/- from target pixel to average

void denoise_wavelet_avgpix(float *temp, float *fimg, int st, int size, int sc)
{
  int ii;

  for (ii = 0; ii < sc; ii++)
    temp[ii] = 0.25*(2*fimg[st*ii] + fimg[st*(sc-ii)] + fimg[st*(ii+sc)]);

  for (; ii < size - sc; ii++)
    temp[ii] = 0.25*(2*fimg[st*ii] + fimg[st*(ii-sc)] + fimg[st*(ii+sc)]);

  for (; ii < size; ii++)
    temp[ii] = 0.25*(2*fimg[st*ii] + fimg[st*(ii-sc)] + fimg[st*(2*size-2-(ii+sc))]);

  return;
}


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

//  dialog to measure noise at mouse click position

void denoise_measure()                                                           //  15.06
{
   using namespace denoise;

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

   char     text[100];
   cchar    *title = ZTX("Measure Noise");
   cchar    *clickmess = ZTX("Click on a monotone image area.");

/***
       ___________________________________
      |        Measure Noise              |
      |  Click on a monotone image area.  |
      |                                   |
      |   mean RGB:   100   150   200     |
      |  mean noise:  1.51  1.23  0.76    |
      |                                   |
      |                         [cancel]  |
      |___________________________________|

***/

   if (zd_denoise_measure) return;

   zdialog *zd = zdialog_new(title,Mwin,Bcancel,null);                           //  smartdenoise dialog
   zd_denoise_measure = zd;
   zdialog_add_widget(zd,"label","clab","dialog",clickmess,"space=3");
   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_run(zd,denoise_measure_dialog_event,"save");                          //  run dialog
   return;
}


//  dialog event and completion function

int denoise_measure_dialog_event(zdialog *zd, cchar *event)
{
   using namespace denoise;

   void denoise_measure_mousefunc();
   
   if (strmatch(event,"escape")) zd->zstat = 1;                                  //  escape = cancel                    15.07
   
   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()                                                 //  improved                           15.07
{
   using namespace denoise;

   char        text[100];
   int         mx, my, px, py, qx, qy, Npix;
   float       *pix3, R;
   float       Rm, Gm, Bm, Rn, Gn, Bn;
   zdialog     *zd = zd_denoise_measure;

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

   if (! LMclick) return;                                                        //  wait for mouse click
   LMclick = 0;
   
   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);

   mx = Mxclick;                                                                 //  mouse click position
   my = Myclick;
   
   if (mx < 13 || mx >= ww-13) return;                                           //  must be 12+ pixels from image edge
   if (my < 13 || my >= hh-13) return;

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

   for (py = my-10; py <= my+10; py++)                                           //  get pixels around mouse position
   for (px = mx-10; px <= mx+10; px++)                                           //  within a radius of 10 
   {                                                                             //  (approx. 314 pixels)
      R = sqrtf((px-mx)*(px-mx) + (py-my)*(py-my));
      if (R > 10) continue;
      Npix++;

      Rm = Gm = Bm = 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);
         Rm += pix3[0];
         Gm += pix3[1];
         Bm += pix3[2];
      }
      
      Rm = Rm / 25;                                                              //  mean RGB for surrounding pixels
      Gm = Gm / 25;
      Bm = Bm / 25;
   
      pix3 = PXMpix(E3pxm,px,py);                                                //  get pixel RGB noise levels
      Rn += fabsf(Rm - pix3[0]);                                                 //   = | pixel RGB - mean RGB |
      Gn += fabsf(Gm - pix3[1]);                                                 //  accumulate noise levels
      Bn += fabsf(Bm - pix3[2]);
   }

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

   Npix = 0;
   Rm = Gm = Bm = 0;
   
   for (py = my-10; py <= my+10; py++)                                           //  get pixels around mouse position
   for (px = mx-10; px <= mx+10; px++)                                           //  within a radius of 10 
   {                                                                             //  (approx. 314 pixels)
      R = sqrtf((px-mx)*(px-mx) + (py-my)*(py-my));
      if (R > 10) continue;
      Npix++;

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

   Rm = Rm / Npix;                                                               //  mean RGB values
   Gm = Gm / Npix;                                                               //    for pixels around mouse
   Bm = Bm / Npix;
   
   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);

   return;
}


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

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

editfunc    EFerase;


//  menu function

void m_smart_erase(GtkWidget *, const char *)
{
   int smart_erase_dialog_event(zdialog* zd, const char *event);

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

   EFerase.menufunc = m_smart_erase;
   EFerase.funcname = "smart_erase";
   EFerase.Farea = 0;                                                            //  select area deleted
   EFerase.mousefunc = sa_mouse_select;                                          //  mouse function (use select area)
   if (! edit_setup(EFerase)) return;                                            //  setup edit

/**    _________________________________________
      |                                         |
      |    1. Drag mouse to select.             |
      |    2. Erase.   3. Repeat.               |
      |                                         |
      | Radius [10|v]    Blur [0.5|v]           |
      | [New Area] [Show] [Hide] [Erase] [Undo] |
      |                                         |
      |                                 [Done]  |
      |_________________________________________|
**/

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

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

   sa_unselect();                                                                //  unselect area if any
   cc = E1pxm->ww * E1pxm->hh * sizeof(uint16);                                  //  create new area
   sa_pixmap = (uint16 *) zmalloc(cc);
   memset(sa_pixmap,0,cc);
   sa_mode = mode_mouse;                                                         //  mode = select by mouse
   sa_stat = 1;                                                                  //  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
   takeMouse(sa_mouse_select,0);                                                 //  use select area mouse function
   sa_show(1);

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


//  dialog event and completion function

int smart_erase_dialog_event(zdialog *zd, const char *event)                     //  overhauled
{
   void smart_erase_func(int mode);
   void smart_erase_blur(float radius);

   float       radius;
   int         cc;

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

   if (zd->zstat)
   {
      sa_unselect();                                                             //  delete 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_unselect();
      cc = E1pxm->ww * E1pxm->hh * sizeof(uint16);                               //  create new area
      sa_pixmap = (uint16 *) zmalloc(cc);
      memset(sa_pixmap,0,cc);
      sa_mode = mode_mouse;                                                      //  mode = select by mouse
      sa_stat = 1;                                                               //  status = active edit
      sa_fww = E1pxm->ww;
      sa_fhh = E1pxm->hh;
      sa_show(1);
      takeMouse(sa_mouse_select,0);
   }

   if (strmatch(event,"show")) {
      sa_stat = 1;                                                               //  status = active edit
      sa_show(1);
      takeMouse(sa_mouse_select,0);
   }

   if (strmatch(event,"hide")) {
      sa_show(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);
   }

   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)
{
   int         px, py, npx, npy;
   int         qx, qy, sx, sy, tx, ty;
   int         ww, hh, sx2, sy2;
   int         ii, rad, inc, cc;
   int         dist2, mindist2;
   float       slope;
   char        *pmap;
   float       *pix1, *pix3;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);                         //  15.09

   if (sa_stat != 3) 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 = (char *) zmalloc(cc);
   memset(pmap,0,cc);

   for (py = sa_miny; py < sa_maxy; py++)                                        //  loop all pixels in area
   for (px = sa_minx; px < sa_maxx; px++)
   {
      ii = py * 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;                                       //  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)) {
         slope = 1.0 * (qx - px) / (qy - py);
         if (qy > py) inc = 1;
         else inc = -1;
         for (sy = py; sy != qy; sy += inc)                                      //  line from px/py to qx/qy
         {
            sx = px + slope * (sy - py);
            ii = sy * 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 > ww-1) tx = ww-1;
            if (ty < 0) ty = 0;
            if (ty > hh-1) ty = hh-1;
            sx2 = sx;                                                            //  skip-over selected pixels          15.03
            sy2 = sy;
            while (tx > 0 && tx < ww && ty > 0 && ty < hh) {
               ii = ty * ww + tx;
               if (! sa_pixmap[ii]) break;
               sy2 += inc;
               sx2 = px + slope * (sy2 - py);
               tx = qx + (qx - sx2);
               ty = qy + (qy - sy2);
            }
            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; 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;
            sx2 = sx;                                                            //  skip-over selected pixels          15.03
            sy2 = sy;
            while (tx > 0 && tx < ww && ty > 0 && ty < hh) {
               ii = ty * ww + tx;
               if (! sa_pixmap[ii]) break;
               sy2 += inc;
               sx2 = px + slope * (sy2 - py);
               tx = qx + (qx - sx2);
               ty = qy + (qy - sy2);
            }
            pix1 = PXMpix(E3pxm,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)
{
   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;                                                   //  15.09

   if (sa_stat != 3) 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 >= E3pxm->ww) continue;                          //  omit pixels off edge
         if (py+dy < 0 || py+dy >= E3pxm->hh) continue;
         adx = abs(dx);
         ady = abs(dy);
         pixN = pix9 + (dy * E3pxm->ww + 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;
}


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

//  red eye removal function

struct sredmem {                                                                 //  red-eye struct in memory
   char        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;
void     redeye_mousefunc();

editfunc    EFredeye;

#define pixred(pix) (25*pix[0]/(pixbright(pix)+1))                               //  red brightness 0-100%


//  menu function

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

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

   F1_help_topic = "redeye_remove";

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

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

   zdialog_add_widget(zd,"label","lab1","dialog",redeye_message);
   zdialog_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, cchar *event)
{
   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   
   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;
}


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

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


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

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

   if (LMclick)                                                                  //  left mouse click
   {
      px = Mxclick;                                                              //  click position
      py = Myclick;
      if (px < 0 || px > E3pxm->ww-1 || py < 0 || py > E3pxm->hh-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 > E3pxm->ww-1) ww = E3pxm->ww-1 - px;
      if (py + hh > E3pxm->hh-1) hh = E3pxm->hh-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)
{
   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)
{
   int      rad, radlim;

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

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

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

   return;
}


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

void redeye_distr(int ii)
{
   int         cx, cy, ww, hh, rad, px, py;
   int         bin, npix, dbins[20], bsum, blim;
   float       rd, maxred, minred, redpart, dbase, dstep;
   char        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 threshhold
   redmem[ii].tstep = dstep;                                                     //  redness step (5% of range)

   return;
}


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

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


//  remove a red-eye from memory

void redeye_remove(int ii)
{
   int      cx, cy, rad, px, py;
   float    *pix1, *pix3;
   int      nc = E1pxm->nc, pcc = nc * sizeof(float);                            //  15.09

   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);                                                     //  erase mouse ellipse
   return;
}


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

int redeye_radlim(int cx, int cy)
{
   int radlim = 100;
   if (cx < 100) radlim = cx;
   if (E3pxm->ww-1 - cx < 100) radlim = E3pxm->ww-1 - cx;
   if (cy < 100) radlim = cy;
   if (E3pxm->hh-1 - cy < 100) radlim = E3pxm->hh-1 - cy;
   return radlim;
}


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

//  Pixel paint function - paint individual pixels with the mouse.
//  The mouse circle pixels have a selected color, or they are taken from the
//  image at a relative position (x,y) from the mouse position (aka "clone").

namespace pclone_names
{
   int   pc_dialog_event(zdialog* zd, cchar *event);
   void  pc_mousefunc();
   void  pc_dopixels(int px, int py);                                            //  update pixel block
   void  pc_savepixB(int px, int py);                                            //  save pixel block for poss. undo
   void  pc_undolastB();                                                         //  undo last pixel block, free memory
   void  pc_freefirstB();                                                        //  free memory for first pixel block
   void  pc_freeallB();                                                          //  free memory for all pixel blocks

   uint8    RGB[3] = { 255, 0, 0 };                                              //  color to paint
   int      mode;                                                                //  1/2 = paint / erase
   int      paint_mode;                                                          //  1/2 = paint color / clone image
   int      Mradius;                                                             //  mouse radius
   int      imagex, imagey;                                                      //  source image location
   float    kernel[400][400];                                                    //  radius <= 199
   int      Fgradual = 1;                                                        //  flag, gradual paint and unpaint/erase
   int      Fpot = 0;                                                            //  flag, paint over transparent areas
   int      nc, ac;                                                              //  no. channels, alpha channel

   int64    maxmem = 2000 * MEGA;                                                //  max. pixel block memory            16.06
   int64    totmem;                                                              //  pixB memory allocated
   int      maxpixB = 10000;                                                     //  max. pixel blocks
   int      totpixB = 0;                                                         //  total pixel blocks
   int      pixBseq = 0;                                                         //  last pixel block sequence no.

   typedef struct {                                                              //  pixel block before edit
      int         seq;                                                           //  block sequence no.
      uint16      px, py;                                                        //  center pixel (radius org.)
      uint16      radius;                                                        //  radius of pixel block
      float       pixel[][4];                                                    //  array of pixel[npix][4]
   }  pixBmem_t;
   
   pixBmem_t   **pixBmem = 0;                                                    //  *pixBmem_t[]

   int   pixBmem_cc = 12;                                                        //  all except pixel array + pad
   int   pcc4 = 4 * sizeof(float);                                               //  pixel cc: RGBA = 4 channels

   editfunc    EFpaintclone;
}


//  menu function

void m_paint_clone(GtkWidget *, cchar *)
{
   using namespace pclone_names;

   cchar    *mess1 = ZTX("shift + left click: pick color or image position \n"
                         "left click or drag: paint color or copy image \n"
                         "right click or drag: remove color or image");

   F1_help_topic = "paint_clone";

   EFpaintclone.menufunc = m_paint_clone;
   EFpaintclone.funcname = "paint_clone";
   EFpaintclone.Farea = 2;                                                       //  select area OK
   EFpaintclone.mousefunc = pc_mousefunc;                                        //  mouse function
   if (! edit_setup(EFpaintclone)) return;                                       //  setup edit

   /********
             __________________________________________________
            |           Paint/Clone                            |
            |                                                  |
            |  (o) paint color [_____]  (o) copy from image    |
            |                                                  |
            |  left click: pick color or location from image   |
            |  left drag: paint color or image from location   |
            |  right drag: remove color or image               |
            |                                                  |
            |  paintbrush radius    [____|-+]     [undo last]  |
            |  transparency center  [____|-+]     [undo all]   |
            |  transparency edge    [____|-+]                  |
            |                                                  |
            |  [x] gradual paint  [x] paint transparent areas  |                 //  16.02
            |                                                  |
            |                                 [done] [cancel]  |
            |__________________________________________________|

   ********/

   zdialog *zd = zdialog_new(ZTX("Paint/Clone"),Mwin,Bdone,Bcancel,null);
   EFpaintclone.zd = zd;

   zdialog_add_widget(zd,"hbox","hbr","dialog",0,"space=3");
   zdialog_add_widget(zd,"radio","paintcolor","hbr",ZTX("paint color"),"space=5");
   zdialog_add_widget(zd,"colorbutt","colorbutt","hbr","255|0|0");
   zdialog_add_widget(zd,"label","space","hbr",0,"space=10");
   zdialog_add_widget(zd,"radio","paintimage","hbr",ZTX("copy from image"));
   zdialog_add_widget(zd,"label","labm","dialog",mess1,"space=5");
   zdialog_add_widget(zd,"hbox","hbbri","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbbr1","hbbri",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbbr2","hbbri",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","space","hbbri",0,"space=10");
   zdialog_add_widget(zd,"vbox","vbbr3","hbbri",0,"space=10");
   zdialog_add_widget(zd,"label","labbr","vbbr1",ZTX("paintbrush radius"));
   zdialog_add_widget(zd,"label","labtc","vbbr1",ZTX("transparency center"));
   zdialog_add_widget(zd,"label","labte","vbbr1",ZTX("transparency edge"));
   zdialog_add_widget(zd,"spin","radius","vbbr2","1|199|1|20");
   zdialog_add_widget(zd,"spin","trcent","vbbr2","0|100|0.1|95");
   zdialog_add_widget(zd,"spin","tredge","vbbr2","0|100|0.1|100");
   zdialog_add_widget(zd,"button","undlast","vbbr3",Bundolast);
   zdialog_add_widget(zd,"button","undall","vbbr3",Bundoall);
   zdialog_add_widget(zd,"hbox","hb4","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","gradual","hb4",ZTX("gradual paint"),"space=5");
   zdialog_add_widget(zd,"check","fpot","hb4",ZTX("paint over transparent areas"),"space=5");

   zdialog_stuff(zd,"paintcolor",1);                                             //  default paint color mode
   zdialog_stuff(zd,"paintimage",0);
   zdialog_stuff(zd,"gradual",1);
   zdialog_stuff(zd,"fpot",0);

   zdialog_restore_inputs(zd);                                                   //  preload prior user inputs
   zdialog_run(zd,pc_dialog_event,"save");                                       //  run dialog, parallel

   zdialog_fetch(zd,"paintcolor",paint_mode);                                    //  1/2 = paint color / image
   if (paint_mode != 1) paint_mode = 2;
   
   zdialog_fetch(zd,"gradual",Fgradual);                                         //  instant/gradual paint
   zdialog_send_event(zd,"colorbutt");                                           //  initialize paint color             15.07
   zdialog_send_event(zd,"radius");                                              //  get kernel initialized

   zdialog_fetch(zd,"fpot",Fpot);                                                //  paint over transparent areas       16.02

   totmem = 0;                                                                   //  memory used
   pixBmem = 0;                                                                  //  pixel block memory
   totpixB = 0;                                                                  //  pixel blocks
   pixBseq = 0;
   imagex = imagey = 0;                                                          //  no clone source pixels
   
   mode = 1;                                                                     //  start with paint mode

   ac = 0;
   nc = E1pxm->nc;                                                               //  channels, RGBA
   if (nc > 3) ac = 1;                                                           //  alpha channel present

   takeMouse(pc_mousefunc,drawcursor);                                           //  connect mouse function
   return;
}


//  dialog event and completion callback function

int pclone_names::pc_dialog_event(zdialog *zd, cchar *event)
{
   using namespace pclone_names;

   char        color[20];
   cchar       *pp;
   int         radius, dx, dy;
   float       rad, kern, trcent, tredge;

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

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

   draw_mousecircle(0,0,0,1);                                                    //  erase mouse circle
   if (paint_mode == 2) draw_mousecircle2(0,0,0,1);                              //  erase source tracking circle

   if (strmatch(event,"focus"))                                                  //  toggle mouse capture
      takeMouse(pc_mousefunc,drawcursor);

   if (strstr("paintcolor paintimage",event))
   {
      zdialog_fetch(zd,"paintcolor",paint_mode);                                 //  1/2 = paint color / paint image
      if (paint_mode != 1) paint_mode = 2;
   }

   if (strmatch(event,"colorbutt"))
   {
      zdialog_fetch(zd,"colorbutt",color,19);                                    //  get paint color from color wheel
      pp = strField(color,"|",1);
      if (pp) RGB[0] = atoi(pp);
      pp = strField(color,"|",2);
      if (pp) RGB[1] = atoi(pp);
      pp = strField(color,"|",3);
      if (pp) RGB[2] = atoi(pp);
   }

   if (strstr("radius trcent tredge",event))                                     //  get new brush attributes
   {
      zdialog_fetch(zd,"radius",Mradius);                                        //  mouse radius
      zdialog_fetch(zd,"trcent",trcent);                                         //  center transparency
      zdialog_fetch(zd,"tredge",tredge);                                         //  edge transparency

      trcent = 0.01 * trcent;                                                    //  scale 0 ... 1
      tredge = 0.01 * tredge;
      tredge = (1 - trcent) * (1 - tredge);
      tredge = 1 - tredge;
      trcent = sqrt(trcent);                                                     //  speed up the curve
      tredge = sqrt(tredge);

      radius = Mradius;

      for (dy = -radius; dy <= radius; dy++)                                     //  build kernel
      for (dx = -radius; dx <= radius; dx++)
      {
         rad = sqrt(dx*dx + dy*dy);
         kern = (radius - rad) / radius;                                         //  center ... edge  >>  1 ... 0
         kern = kern * (trcent - tredge) + tredge;                               //  transparency  center ... edge
         if (kern < 0) kern = 0;                                                 //  transp. 0 ... 100%  >>  kern 0 ... 1
         if (kern > 1) kern = 1;
         if (rad > radius) kern = 2;                                             //  beyond radius, within square
         kernel[dx+radius][dy+radius] = kern;
      }
   }

   if (strmatch(event,"undlast"))                                                //  undo last edit (click or drag)
      pc_undolastB();

   if (strmatch(event,"undall")) {                                               //  undo all edits
      edit_reset();
      pc_freeallB();
   }

   if (strmatch(event,"gradual"))                                                //  flag, gradual overpaints
      zdialog_fetch(zd,"gradual",Fgradual);
   
   if (strmatch(event,"fpot"))                                                   //  flag, paint over transparency      16.02
      zdialog_fetch(zd,"fpot",Fpot);

   return 1;
}


//  pixel paint mouse function

void pclone_names::pc_mousefunc()
{
   using namespace pclone_names;

   static int  pmxdown = 0, pmydown = 0;
   int         px, py;
   char        color[20];
   float       *pix3;
   zdialog     *zd = EFpaintclone.zd;

   if (LMclick && KBshiftkey)                                                    //  shift + left mouse click
   {
      if (paint_mode == 1) {                                                     //  paint color mode
         px = Mxclick;
         py = Myclick;
         pix3 = PXMpix(E3pxm,px,py);                                             //  pick new color from image
         RGB[0] = pix3[0];
         RGB[1] = pix3[1];
         RGB[2] = pix3[2];
         snprintf(color,19,"%d|%d|%d",RGB[0],RGB[1],RGB[2]);
         if (zd) zdialog_stuff(zd,"colorbutt",color);
      }

      if (paint_mode == 2) {                                                     //  paint image mode
         imagex = Mxclick;                                                       //  new source image location
         imagey = Myclick;
      }
   }

   else if (LMclick || RMclick)
   {
      if (LMclick) mode = 1;                                                     //  left click, paint
      if (RMclick) mode = 2;                                                     //  right click, erase

      px = Mxclick;
      py = Myclick;

      pixBseq++;                                                                 //  new undo seq. no.

      draw_mousecircle(0,0,0,1);                                                 //  erase mouse circle
      if (paint_mode == 2) draw_mousecircle2(0,0,0,1);                           //  erase source tracking circle

      pc_dopixels(px,py);                                                        //  do 1 block of pixels
   }

   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
         pixBseq++;                                                              //  new undo seq. no.
         pmxdown = Mxdown;
         pmydown = Mydown;
      }

      draw_mousecircle(0,0,0,1);                                                 //  erase mouse circle
      if (paint_mode == 2) draw_mousecircle2(0,0,0,1);                           //  erase source tracking circle

      pc_dopixels(px,py);                                                        //  do 1 block of pixels
   }

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

   if (mode == 1 && paint_mode == 2 && (Mxdown || Mydown)) {                     //  2nd circle tracks source pixels
      px = imagex + Mxposn - Mxdown;
      py = imagey + Myposn - Mydown;
      if (px > 0 && px < E3pxm->ww-1 && py > 0 && py < E3pxm->hh-1);
         draw_mousecircle2(px,py,Mradius,0);
   }
   else draw_mousecircle2(0,0,0,1);                                              //  no 2nd circle

   LMclick = RMclick = Mxdrag = Mydrag = 0;
   return;
}


//  paint or erase 1 block of pixels within mouse radius of px, py

void pclone_names::pc_dopixels(int px, int py)
{
   using namespace pclone_names;

   float       *pix1, *pix3, *pix9;
   int         radius, dx, dy, qx, qy, sx, sy;
   int         ii, ww, hh, dist = 0;
   int         pot = ac * Fpot;                                                  //  paint over transparent areas       16.02
   float       red, green, blue;
   float       kern;

   if (paint_mode == 2 && ! imagex && ! imagey) return;                          //  no source area defined

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

   pc_savepixB(px,py);                                                           //  save pixels for poss. undo

   red = RGB[0];                                                                 //  paint color
   green = RGB[1];
   blue = RGB[2];

   radius = Mradius;

   if (mode == 1) {
      CEF->Fmods++;
      CEF->Fsaved = 0;
   }

   for (dy = -radius; dy <= radius; dy++)                                        //  loop surrounding block of pixels
   for (dx = -radius; dx <= radius; dx++)
   {
      qx = px + dx;
      qy = py + dy;

      if (qx < 0 || qx > ww-1) continue;
      if (qy < 0 || qy > hh-1) continue;

      if (sa_stat == 3) {                                                        //  select area active
         ii = qy * ww + qx;
         dist = sa_pixmap[ii];
         if (! dist) continue;                                                   //  pixel is outside area
      }

      kern = kernel[dx+radius][dy+radius];                                       //  mouse transparencies
      if (kern > 1) continue;                                                    //  outside mouse radius

      if (! Fgradual) kern = 0;                                                  //  sudden paint or erase

      if (sa_stat == 3 && dist < sa_blend)                                       //  at select area edge,
///      kern = kern + (1.0 - kern) * (sa_blend - dist) / sa_blend;              //    transparency = 100%
         kern = 1.0 - (1.0 - kern) * sa_blendfunc(dist);                         //    transparency = 100%              16.08

      pix1 = PXMpix(E1pxm,qx,qy);                                                //  source image pixel
      pix3 = PXMpix(E3pxm,qx,qy);                                                //  edited image pixel

      if (mode == 1 && paint_mode == 1)                                          //  paint with color
      {
         if (Fgradual) {
            pix3[0] = (1.0 - kern) * red   + kern * pix3[0];                     //  overpaints accumulate
            pix3[1] = (1.0 - kern) * green + kern * pix3[1];
            pix3[2] = (1.0 - kern) * blue  + kern * pix3[2];
            if (pot) pix3[3] = (1.0 - kern) * 255.0  + kern * pix3[3];           //  16.02
         }
         else {                                                                  //  paint sudden
            pix3[0] = (1.0 - kern) * red   + kern * pix1[0];
            pix3[1] = (1.0 - kern) * green + kern * pix1[1];
            pix3[2] = (1.0 - kern) * blue  + kern * pix1[2];
            if (pot) pix3[3] = (1.0 - kern) * 255.0  + kern * pix1[3];           //  16.02
         }
      }

      if (mode == 1 && paint_mode == 2)                                          //  paint from other image location (clone)
      {
         sx = imagex + qx - Mxdown;                                              //  image location + mouse drag shift
         sy = imagey + qy - Mydown;
         if (sx < 0) sx = 0;
         if (sx > ww-1) sx = ww-1;
         if (sy < 0) sy = 0;
         if (sy > hh-1) sy = hh-1;
         pix9 = PXMpix(E1pxm,sx,sy);                                             //  source image pixel at location

         if (Fgradual) {
            pix3[0] = (1.0 - kern) * pix9[0] + kern * pix3[0];                   //  overpaints accumulate
            pix3[1] = (1.0 - kern) * pix9[1] + kern * pix3[1];
            pix3[2] = (1.0 - kern) * pix9[2] + kern * pix3[2];
            if (pot) pix3[3] = (1.0 - kern) * pix9[3] + kern * pix3[3];          //  16.02
         }
         else {
            pix3[0] = (1.0 - kern) * pix9[0] + kern * pix1[0];                   //  paint sudden
            pix3[1] = (1.0 - kern) * pix9[1] + kern * pix1[1];
            pix3[2] = (1.0 - kern) * pix9[2] + kern * pix1[2];
            if (pot) pix3[3] = (1.0 - kern) * pix9[3] + kern * pix1[3];          //  16.02
         }
      }

      if (mode == 2)                                                             //  unpaint or erase
      {
         if (Fgradual) {
            pix3[0] = (1.0 - kern) * pix1[0] + kern * pix3[0];                   //  gradual erase
            pix3[1] = (1.0 - kern) * pix1[1] + kern * pix3[1];
            pix3[2] = (1.0 - kern) * pix1[2] + kern * pix3[2];
            if (pot) pix3[3] = (1.0 - kern) * pix1[3] + kern * pix3[3];          //  16.02
         }
         else {
            pix3[0] = pix1[0];                                                   //  sudden erase
            pix3[1] = pix1[1];
            pix3[2] = pix1[2];
            if (pot) pix3[3] = pix1[3];                                          //  16.02
         }
      }
   }

   px = px - radius - 1;                                                         //  repaint modified area
   py = py - radius - 1;
   ww = 2 * radius + 3;
   Fpaint3(px,py,ww,ww);

   return;
}


//  save 1 block of pixels for possible undo

void pclone_names::pc_savepixB(int px, int py)
{
   using namespace pclone_names;

   int            cc, npix, radius, dx, dy;
   float          *pix3;
   pixBmem_t      *pclonesave1;
   
   if (! pixBmem) {                                                              //  first time
      pixBmem = (pixBmem_t **) zmalloc(maxpixB * sizeof(void *));
      totpixB = 0;
      totmem = 0;
   }

   if (totmem > maxmem || totpixB == maxpixB)                                    //  free memory for oldest updates     16.06 
      while (totmem > 0.7 * maxmem || totpixB > 0.7 * maxpixB)
         pc_freefirstB();                                     

   radius = Mradius;
   npix = 0;

   for (dy = -radius; dy <= radius; dy++)                                        //  count pixels in block
   for (dx = -radius; dx <= radius; dx++)
   {
      if (px + dx < 0 || px + dx > E3pxm->ww-1) continue;
      if (py + dy < 0 || py + dy > E3pxm->hh-1) continue;
      npix++;
   }

   cc = npix * pcc4 + pixBmem_cc;                                                //  16.02
   pclonesave1 = (pixBmem_t *) zmalloc(cc);                                      //  allocate memory for block
   pixBmem[totpixB] = pclonesave1;
   totpixB += 1;
   totmem += cc;                                                                 //  15.08

   pclonesave1->seq = pixBseq;                                                   //  save pixel block poop
   pclonesave1->px = px;
   pclonesave1->py = py;
   pclonesave1->radius = radius;

   npix = 0;

   for (dy = -radius; dy <= radius; dy++)                                        //  save pixels in block
   for (dx = -radius; dx <= radius; dx++)
   {
      if (px + dx < 0 || px + dx > E3pxm->ww-1) continue;
      if (py + dy < 0 || py + dy > E3pxm->hh-1) continue;
      pix3 = PXMpix(E3pxm,(px+dx),(py+dy));                                      //  edited image pixel
      pclonesave1->pixel[npix][0] = pix3[0];
      pclonesave1->pixel[npix][1] = pix3[1];
      pclonesave1->pixel[npix][2] = pix3[2];
      if (ac) pclonesave1->pixel[npix][3] = pix3[3];                             //  16.02
      npix++;
   }

   return;
}


//  undo last pixel block (newest edit) and free memory

void pclone_names::pc_undolastB()
{
   using namespace pclone_names;

   int            ii, cc, npix, radius;
   int            ww, px, py, dx, dy;
   float          *pix3;
   pixBmem_t      *pclonesave1;

   for (ii = totpixB-1; ii >= 0; ii--)
   {
      pclonesave1 = pixBmem[ii];
      if (pclonesave1->seq != pixBseq) break;
      px = pclonesave1->px;
      py = pclonesave1->py;
      radius = pclonesave1->radius;

      npix = 0;
      for (dy = -radius; dy <= radius; dy++)
      for (dx = -radius; dx <= radius; dx++)
      {
         if (px + dx < 0 || px + dx > E3pxm->ww-1) continue;
         if (py + dy < 0 || py + dy > E3pxm->hh-1) continue;
         pix3 = PXMpix(E3pxm,(px+dx),(py+dy));
         pix3[0] = pclonesave1->pixel[npix][0];
         pix3[1] = pclonesave1->pixel[npix][1];
         pix3[2] = pclonesave1->pixel[npix][2];
         if (ac) pix3[3] = pclonesave1->pixel[npix][3];                          //  16.02
         npix++;
      }

      px = px - radius - 1;
      py = py - radius - 1;
      ww = 2 * radius + 3;
      Fpaint3(px,py,ww,ww);

      zfree(pclonesave1);
      pixBmem[ii] = 0;
      cc = npix * pcc4 + pixBmem_cc;                                             //  16.02
      totmem -= cc;
      totpixB--;
   }

   if (pixBseq > 0) --pixBseq;
   return;
}


//  free memory for first pixel (oldest edit)

void pclone_names::pc_freefirstB()                                               //  16.06
{
   using namespace pclone_names;

   int            firstseq;
   int            ii, jj, cc, npix, radius;
   int            px, py, dx, dy;
   pixBmem_t      *pclonesave1;
   
   if (! totpixB) return;
   firstseq = pixBmem[0]->seq;
   
   for (ii = 0; ii < totpixB; ii++)
   {
      pclonesave1 = pixBmem[ii];
      if (pclonesave1->seq != firstseq) break;
      px = pclonesave1->px;
      py = pclonesave1->py;
      radius = pclonesave1->radius;
      npix = 0;
      for (dy = -radius; dy <= radius; dy++)
      for (dx = -radius; dx <= radius; dx++)
      {
         if (px + dx < 0 || px + dx > E3pxm->ww-1) continue;
         if (py + dy < 0 || py + dy > E3pxm->hh-1) continue;
         npix++;
      }

      zfree(pclonesave1);
      pixBmem[ii] = 0;
      cc = npix * pcc4 + pixBmem_cc;
      totmem -= cc;
   }
   
   for (jj = 0; ii < totpixB; jj++, ii++)
      pixBmem[jj] = pixBmem[ii];   
   
   totpixB = jj;
   return;
}


//  free all pixel block memory

void pclone_names::pc_freeallB()
{
   using namespace pclone_names;

   int            ii;
   pixBmem_t      *pclonesave1;

   for (ii = totpixB-1; ii >= 0; ii--)
   {
      pclonesave1 = pixBmem[ii];
      zfree(pclonesave1);
   }

   if (pixBmem) zfree(pixBmem);
   pixBmem = 0;

   pixBseq = 0;
   totpixB = 0;
   totmem = 0;

   return;
}


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

//  Paint transparency function - paint pixel alpha channel with the mouse.

namespace ptransp_names
{
   int   pt_dialog_event(zdialog* zd, cchar *event);
   void  pt_mousefunc();
   void  pt_dopixels(int px, int py);

   int      mode = 1;                                                            //  1/2 = more/less transparency
   int      Fgradual = 1;                                                        //  gradual or sudden transparency
   int      Mradius;                                                             //  mouse radius
   float    kernel[400][400];                                                    //  radius <= 199

   editfunc    EFpaintransp;
}


//  menu function

void m_paint_transp(GtkWidget *, cchar *)                                        //  15.08
{
   using namespace ptransp_names;

   cchar    *mess1 = ZTX("left drag: add transparency \n"
                         "right drag: add opacity");

   F1_help_topic = "paint_transp";

   EFpaintransp.menufunc = m_paint_transp;
   EFpaintransp.funcname = "paint_transp";
   EFpaintransp.Farea = 2;                                                       //  select area OK
   EFpaintransp.mousefunc = pt_mousefunc;                                        //  mouse function
   if (! edit_setup(EFpaintransp)) return;                                       //  setup edit

   PXM_addalpha(E1pxm);                                                          //  add an alpha channel if req.
   PXM_addalpha(E3pxm);
   Fpaintnow();

   /***
             ____________________________________
            |       Paint transparency           |
            |                                    |
            |  left drag: add transparency       |
            |  right drag: add opacity           |
            |                                    |
            |  paintbrush radius  [____|-+]      |
            |  strength center    [____|-+]      |
            |  strength edge      [____|-+]      |
            |  [x] gradual paint                 |
            |                                    |
            |                   [done] [cancel]  |
            |____________________________________|

   ***/

   zdialog *zd = zdialog_new(ZTX("Paint Transparency"),Mwin,Bdone,Bcancel,null);
   EFpaintransp.zd = zd;

   zdialog_add_widget(zd,"hbox","hbr","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labm","dialog",mess1,"space=5");
   zdialog_add_widget(zd,"hbox","hbbri","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbbr1","hbbri",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbbr2","hbbri",0,"homog|space=5");
   zdialog_add_widget(zd,"label","labbr","vbbr1",ZTX("paintbrush radius"));
   zdialog_add_widget(zd,"label","labsc","vbbr1",ZTX("strength center"));
   zdialog_add_widget(zd,"label","labse","vbbr1",ZTX("strength edge"));
   zdialog_add_widget(zd,"spin","radius","vbbr2","1|199|1|30");
   zdialog_add_widget(zd,"spin","stcent","vbbr2","0|100|1|95");
   zdialog_add_widget(zd,"spin","stedge","vbbr2","0|100|1|100");
   zdialog_add_widget(zd,"hbox","hb4","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","gradual","hb4",ZTX("gradual paint"),"space=5");
   
   zdialog_stuff(zd,"gradual",1);

   zdialog_restore_inputs(zd);                                                   //  preload prior user inputs

   zdialog_run(zd,pt_dialog_event,"save");                                       //  run dialog, parallel
   zdialog_send_event(zd,"radius");                                              //  get kernel initialized

   zdialog_fetch(zd,"gradual",Fgradual);                                         //  instant/gradual paint
   mode = 1;                                                                     //  start with paint mode

   takeMouse(pt_mousefunc,drawcursor);                                           //  connect mouse function
   return;
}


//  dialog event and completion callback function

int ptransp_names::pt_dialog_event(zdialog *zd, cchar *event)
{
   using namespace ptransp_names;

   int         radius, dx, dy;
   float       rad, kern, stcent, stedge;

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

   erase_topcircles();

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

   if (strstr("radius stcent stedge",event))                                     //  get new brush attributes
   {
      zdialog_fetch(zd,"radius",Mradius);                                        //  mouse radius
      zdialog_fetch(zd,"stcent",stcent);                                         //  center transparency
      zdialog_fetch(zd,"stedge",stedge);                                         //  edge transparency

      stcent = 0.01 * stcent;                                                    //  scale 0 ... 1
      stedge = 0.01 * stedge;

      radius = Mradius;

      for (dy = -radius; dy <= radius; dy++)                                     //  build kernel
      for (dx = -radius; dx <= radius; dx++)
      {
         rad = sqrt(dx*dx + dy*dy);
         kern = (radius - rad) / radius;                                         //  center ... edge  >>  1 ... 0
         kern = kern * (stcent - stedge) + stedge;                               //  strength  center ... edge
         if (kern < 0) kern = 0;
         if (kern > 1) kern = 1;
         if (rad > radius) kern = 2;                                             //  beyond radius, within square
         kernel[dx+radius][dy+radius] = kern;
      }
   }

   if (strmatch(event,"gradual"))                                                //  flag, gradual overpaints
      zdialog_fetch(zd,"gradual",Fgradual);
   
   return 1;
}


//  pixel paint mouse function

void ptransp_names::pt_mousefunc()
{
   using namespace ptransp_names;

   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;
      pt_dopixels(px,py);                                                        //  do 1 block of pixels
   }

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

      pt_dopixels(px,py);                                                        //  do 1 block of pixels
   }

   LMclick = RMclick = Mxdrag = Mydrag = 0;
   draw_mousecircle(Mxposn,Myposn,Mradius,0);                                    //  draw mouse circle

   return;
}


//  paint or erase 1 block of pixels within mouse radius of px, py

void ptransp_names::pt_dopixels(int px, int py)
{
   using namespace ptransp_names;

   float       *pix3;
   int         radius, dx, dy, qx, qy;
   int         ii, ww, hh, dist = 0;
   float       kern;

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

   radius = Mradius;

   for (dy = -radius; dy <= radius; dy++)                                        //  loop surrounding block of pixels
   for (dx = -radius; dx <= radius; dx++)
   {
      qx = px + dx;
      qy = py + dy;

      if (qx < 0 || qx > ww-1) continue;
      if (qy < 0 || qy > hh-1) continue;

      if (sa_stat == 3) {                                                        //  select area active
         ii = qy * ww + qx;
         dist = sa_pixmap[ii];
         if (! dist) continue;                                                   //  pixel is outside area
      }

      kern = kernel[dx+radius][dy+radius];                                       //  mouse transparencies
      if (kern > 1) continue;                                                    //  outside mouse radius

      if (sa_stat == 3 && dist < sa_blend)                                       //  within blend distance,
///      kern = kern * (1.0 - (sa_blend - dist) / sa_blend);                     //    kern = kern ... 0 at edge
         kern = kern * sa_blendfunc(dist);                                       //  16.08
         
      pix3 = PXMpix(E3pxm,qx,qy);                                                //  edited image pixel

      if (mode == 1) {                                                           //  add transparency
         if (Fgradual) {
            pix3[3] -= 2 * kern;                                                 //  accumulate transparency
            if (pix3[3] < 0) pix3[3] = 0;
         }
         else pix3[3] = 0;                                                       //  instant transparency
      }

      if (mode == 2) {                                                           //  add opacity
         if (Fgradual) { 
            pix3[3] += 2 * kern;                                                 //  accumulate 
            if (pix3[3] > 255.0) pix3[3] = 255.0;
         }
         else pix3[3] = 255.0;                                                   //  instant
      }
   }

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

   px = px - radius - 1;                                                         //  repaint modified area              15.09
   py = py - radius - 1;
   ww = 2 * radius + 3;
   Fpaint3(px,py,ww,ww);

   return;
}


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

//  Add transparency function
//  Add transparency proportional to brightness or match with selected color.

namespace add_transp_names
{
   int   dialog_event(zdialog* zd, cchar *event);
   void  mousefunc();
   void  *thread(void *);
   void  *wthread(void *);
   
   int      ww, hh;
   int      Hu, Su, Lu;                                                          //  "match using" flags, 0 or 1
   float    Rm, Gm, Bm;                                                          //  RGB image color to match
   float    Hm, Sm, Lm;                                                          //  corresp. HSL color
   int      Fbrightness, Fcolor, Finvert;
   float    matchlevel, transparency, threshold;
   float    Rmatch, Gmatch, Bmatch;

   editfunc    EFaddtransp;
}

void HSLtoRGB(float H, float S, float L, float &R, float &G, float &B);
void RGBtoHSL(float R, float G, float B, float &H, float &S, float &L);


//  menu function

void m_add_transp(GtkWidget *, cchar *)                                          //  16.03
{
   using namespace add_transp_names;
   
   F1_help_topic = "add_transparency";

   EFaddtransp.menufunc = m_add_transp;
   EFaddtransp.funcname = "add_transp";
   EFaddtransp.Farea = 2;                                                        //  select area OK
   EFaddtransp.mousefunc = mousefunc;
   EFaddtransp.threadfunc = thread;

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

   PXM_addalpha(E1pxm);                                                          //  add an alpha channel if req.
   PXM_addalpha(E3pxm);
   Fpaintnow();

   /***
             ___________________________________________________
            |               Add Transparency                    |
            |                                                   |
            |  [x] Match Brightness                             |
            |                                                   |
            |  [x] Match Color [######]                         |
            |  Match using: [] Hue  [] Saturation  [] Lightness |
            |  Match Level: =================[]========== 100%  |
            |                                                   |
            |  [x] Invert Match                                 |
            |  Transparency =================[]==============   |     transparency level 0-100%
            |  Threshold ===========[]=======================   |     transparency threshold 
            |                                                   |
            |                                  [done] [cancel]  |
            |___________________________________________________|

   ***/

   zdialog *zd = zdialog_new(ZTX("Add Transparency"),Mwin,Bdone,Bcancel,null);
   EFaddtransp.zd = zd;

   zdialog_add_widget(zd,"hbox","hbmb","dialog");
   zdialog_add_widget(zd,"check","Fbrightness","hbmb",ZTX("Match Brightness"),"space=3");

   zdialog_add_widget(zd,"hsep","hsbc","dialog");

   zdialog_add_widget(zd,"hbox","hbmc","dialog");
   zdialog_add_widget(zd,"check","Fcolor","hbmc",ZTX("Match Color"),"space=3");
   zdialog_add_widget(zd,"colorbutt","matchRGB","hbmc","0|0|0","space=3");
   zdialog_add_ttip(zd,"matchRGB",ZTX("click on image to select color"));

   zdialog_add_widget(zd,"hbox","hbmu","dialog");
   zdialog_add_widget(zd,"label","labmu","hbmu",ZTX("Match using:"),"space=5");
   zdialog_add_widget(zd,"check","Hu","hbmu","Hue","space=3");
   zdialog_add_widget(zd,"check","Su","hbmu","Saturation","space=3");
   zdialog_add_widget(zd,"check","Lu","hbmu","Lightness","space=3");
   
   zdialog_add_widget(zd,"hbox","hbml","dialog");
   zdialog_add_widget(zd,"label","labml","hbml",ZTX("Match Level:"),"space=5");
   zdialog_add_widget(zd,"hscale","matchlevel","hbml","0|1|0.001|1.0","expand");
   zdialog_add_widget(zd,"label","lab100%","hbml","100%","space=4");

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

   zdialog_add_widget(zd,"hbox","hbinv","dialog");
   zdialog_add_widget(zd,"check","Finvert","hbinv",ZTX("Invert Match"),"space=3");

   zdialog_add_widget(zd,"hbox","hbtran","dialog");
   zdialog_add_widget(zd,"label","labint","hbtran",ZTX("Transparency"),"space=5");
   zdialog_add_widget(zd,"hscale","transparency","hbtran","0.0|1.0|0.001|0.0","space=5|expand");

   zdialog_add_widget(zd,"hbox","hbthresh","dialog");
   zdialog_add_widget(zd,"label","labthr","hbthresh",Bthresh,"space=5");
   zdialog_add_widget(zd,"hscale","threshold","hbthresh","0.0|1.0|0.001|0.0","space=5|expand");
   
   zdialog_stuff(zd,"Fbrightness",1);                                            //  defaults
   zdialog_stuff(zd,"Fcolor",0);
   zdialog_stuff(zd,"Finvert",0);

   zdialog_stuff(zd,"Hu",1);
   zdialog_stuff(zd,"Su",1);
   zdialog_stuff(zd,"Lu",1);

   Fbrightness = 1;
   Fcolor = Finvert = 0;
   matchlevel = 1.0;
   transparency = threshold = 0.0;
   
   Rm = Gm = Bm = 0;                                                             //  color to match = black = not set
   Hm = Sm = Lm = 0;
   Hu = Su = Lu = 1;

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

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


//  dialog event and completion callback function

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

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(0);                                          //  [done]
      else edit_cancel(0);                                                       //  [cancel] or [x]
      return 1;
   }

   if (strmatch(event,"Fbrightness")) {
      zdialog_fetch(zd,"Fbrightness",Fbrightness);
      Fcolor = 1 - Fbrightness;
      zdialog_stuff(zd,"Fcolor",Fcolor);
   }

   if (strmatch(event,"Fcolor")) {
      zdialog_fetch(zd,"Fcolor",Fcolor);
      Fbrightness = 1 - Fcolor;
      zdialog_stuff(zd,"Fbrightness",Fbrightness);
   }
   
   zdialog_fetch(zd,"Fbrightness",Fbrightness);                                  //  get all dialog controls
   zdialog_fetch(zd,"Fcolor",Fcolor);
   zdialog_fetch(zd,"Finvert",Finvert);
   zdialog_fetch(zd,"matchlevel",matchlevel);
   zdialog_fetch(zd,"transparency",transparency);
   zdialog_fetch(zd,"threshold",threshold);
   zdialog_fetch(zd,"Hu",Hu);
   zdialog_fetch(zd,"Su",Su);
   zdialog_fetch(zd,"Lu",Lu);

   signal_thread();                                                              //  set pixel transparencies

   return 1;
}


//  mouse function
//  click on image to set the color to match

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

   char        mcolor[20];
   float       *pix1;
   float       f256 = 1.0 / 256.0;
   zdialog     *zd = EFaddtransp.zd;
   
   if (LMclick)                                                                  //  left mouse click
   {
      LMclick = 0;
      pix1 = PXMpix(E1pxm,Mxclick,Myclick);                                      //  pick match color from image
      Rm = pix1[0];
      Gm = pix1[1];
      Bm = pix1[2];
      snprintf(mcolor,19,"%.0f|%.0f|%.0f",Rm,Gm,Bm);
      zdialog_stuff(zd,"matchRGB",mcolor);

      Rm *= f256;
      Gm *= f256;
      Bm *= f256;
      RGBtoHSL(Rm,Gm,Bm,Hm,Sm,Lm);                                               //  HSL color to match

      zdialog_stuff(zd,"Fcolor",1);
      zdialog_stuff(zd,"Fbrightness",0);
      Fbrightness = 0;
      Fcolor = 1;
      signal_thread();
   }

   return;
}


//  thread function - set pixel transparencies from user inputs

void * add_transp_names::thread(void *)
{
   using namespace add_transp_names;
   
   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request

      for (int ii = 0; ii < NWT; ii++)                                           //  start worker threads
         start_wthread(wthread,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

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

   return 0;                                                                     //  not executed, stop warning
}


void * add_transp_names::wthread(void *arg)                                      //  worker thread function
{
   using namespace add_transp_names;

   int      index = *((int *) (arg));
   int      ii, px, py, dist = 0;
   float    f1, f2, alfa;
   float    pixbrite, pixmatch;
   float    *pix1, *pix3;
   float    red, green, blue;
   float    R1, G1, B1;
   float    H1, S1, L1, dH, dS, dL;
   float    f256 = 1.0 / 256.0;

   for (py = index; py < hh; py += NWT)
   for (px = 0; px < ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * ww + px;
         dist = sa_pixmap[ii];
         if (! dist) continue;                                                   //  pixel is outside area
      }
      
      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      red = pix1[0];
      green = pix1[1];
      blue = pix1[2];
      
      pixmatch = 0;
      
      if (Fbrightness) {                                                         //  match pixel to brightness
         pixbrite = red;
         if (green > pixbrite) pixbrite = green;
         if (blue > pixbrite) pixbrite = blue;
         pixmatch = 0.003906 * pixbrite;                                         //  0 .. 1 for brightness 0 .. 256
         pixmatch = pixmatch * pixmatch;                                         //  increase sensitivity
         pixmatch = pixmatch * pixmatch;
      }

      else                                                                       //  match pixel to HSL color
      {
         R1 = f256 * red;
         G1 = f256 * green;
         B1 = f256 * blue;
         
         RGBtoHSL(R1,G1,B1,H1,S1,L1);                                            //  convert to HSL

         pixmatch = 1.0;                                                         //  compare pixel to match HSL

         if (Hu) {
            dH = fabsf(Hm - H1);
            if (360 - dH < dH) dH = 360 - dH;
            dH *= 0.002778;                                                      //  H difference, normalized 0..1
            pixmatch = 1.0 - dH;
         }

         if (Su) {
            dS = fabsf(Sm - S1);                                                 //  S difference, 0..1
            pixmatch *= (1.0 - dS);
         }

         if (Lu) {
            dL = fabsf(Lm - L1);                                                 //  L difference, 0..1
            pixmatch *= (1.0 - dL);
         }

         pixmatch = pixmatch * pixmatch;                                         //  increase sensitivity
         pixmatch = pixmatch * pixmatch;
      }

      if (Finvert) pixmatch = 1.0 - pixmatch;                                    //  invert match if required

      f1 = matchlevel;                                                           //  reduce for lower match level
      f2 = 1.0 - f1;
      pixmatch = f1 * pixmatch + f2;
      
      pixmatch = pixmatch * transparency;                                        //  reduce for lower transparency
      
      if (pixmatch < threshold) pixmatch = 0;                                    //  eliminate below threshold

      alfa = 255.0 - 255.0 * pixmatch;                                           //  255 .. 0 for pixmatch 0 .. 1

      if (sa_stat == 3 && dist < sa_blend) {                                     //  within area blend distance
///      f1 = 1.0 * dist / sa_blend;                                             //  reduced transparency
         f1 = sa_blendfunc(dist);                                                //  16.08
         f2 = 1.0 - f1;
         alfa = f1 * alfa + f2 * 255.0;
      }

      pix3[3] = alfa;
   }

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


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

//  make a black & white or color positive or negative, or sepia image

namespace colormode_names
{
   editfunc    EFcolormode;
   int         mode;
   float       blend;
}


//  menu function

void m_color_mode(GtkWidget *, cchar *menu)
{
   using namespace colormode_names;

   int    colormode_dialog_event(zdialog *zd, cchar *event);
   void * colormode_thread(void *);

   F1_help_topic = "color_mode";

   EFcolormode.menuname = menu;
   EFcolormode.menufunc = m_color_mode;
   EFcolormode.funcname = "color_mode";
   EFcolormode.threadfunc = colormode_thread;
   EFcolormode.Farea = 2;                                                        //  select area usable
   EFcolormode.Frestart = 1;                                                     //  allow restart
   EFcolormode.Fscript = 1;                                                      //  scripting supported                15.10

   if (! edit_setup(EFcolormode)) return;                                        //  setup edit: no preview

/***
          ___________________________________
         |          Color Mode               |
         |                                   |
         | (o) no change                     |
         | (o) black/white positive          |
         | (o) black/white negative          |
         | (o) color positive                |
         | (o) color negative                |
         | (o) sepia                         |
         |  0% =============[]======== 100%  |                                   //  16.08
         |                                   |
         |                   [Done] [Cancel] |
         |___________________________________|

***/

   zdialog *zd = zdialog_new(ZTX("Color Mode"),Mwin,Bdone,Bcancel,null);
   EFcolormode.zd = zd;

   zdialog_add_widget(zd,"radio","none","dialog",ZTX("no change"));
   zdialog_add_widget(zd,"radio","b&wpos","dialog",ZTX("black/white positive"));
   zdialog_add_widget(zd,"radio","b&wneg","dialog",ZTX("black/white negative"));
   zdialog_add_widget(zd,"radio","colpos","dialog",ZTX("color positive"));
   zdialog_add_widget(zd,"radio","colneg","dialog",ZTX("color negative"));
   zdialog_add_widget(zd,"radio","sepia","dialog",ZTX("sepia"));
   zdialog_add_widget(zd,"hbox","hbblend","dialog");
   zdialog_add_widget(zd,"label","lab0","hbblend","0%","space=5");
   zdialog_add_widget(zd,"hscale","blend","hbblend","0.0|1.0|0.01|1.0","expand");
   zdialog_add_widget(zd,"label","lab100","hbblend","100%","space=5");

   zdialog_resize(zd,200,0);
   zdialog_run(zd,colormode_dialog_event,"save");                                //  run dialog - parallel

   return;
}


//  dialog event and completion callback function

int colormode_dialog_event(zdialog *zd, cchar *event)
{
   using namespace colormode_names;
   
   int      ii;

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   
   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }
   
   if (strmatch(event,"focus")) return 1;
   
   zdialog_fetch(zd,"none",ii);
   if (ii) mode = 0;
   zdialog_fetch(zd,"b&wpos",ii);
   if (ii) mode = 1;
   zdialog_fetch(zd,"b&wneg",ii);
   if (ii) mode = 2;
   zdialog_fetch(zd,"colpos",ii);
   if (ii) mode = 3;
   zdialog_fetch(zd,"colneg",ii);
   if (ii) mode = 4;
   zdialog_fetch(zd,"sepia",ii);
   if (ii) mode = 5;
   zdialog_fetch(zd,"blend",blend);                                              //  added 16.08
   
   if (mode == 0 || mode == 3) {
      edit_reset();
      return 1;
   }
   
   signal_thread();
   return 1;
}


//  thread function

void * colormode_thread(void *)                                                  //  use threads 16.08
{
   using namespace colormode_names;

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

      for (int ii = 0; ii < NWT; ii++)                                           //  start worker threads
         start_wthread(colormode_wthread,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

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

      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop warning
}


//  worker thread functions

void * colormode_wthread(void *arg)
{
   using namespace colormode_names;

   int         index = *((int *) (arg));
   int         ii, dist, px, py;
   float       red1, green1, blue1;
   float       red3, green3, blue3;
   float       *pix1, *pix3;
   float       brite, ff1, ff2;

   dist = 0;                                                                     //  stop compiler warnings
   red3 = green3 = blue3 = 0;

   for (py = index; py < E3pxm->hh; py += NWT)
   for (px = 0; px < E3pxm->ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E1pxm->ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel outside area
      }

      pix1 = PXMpix(E1pxm,px,py);

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

      switch (mode)
      {
         case 0: {                                                               //  no change
            exit_wthread();
            return 0;
         }

         case 1: {                                                               //  black and white positive
            red3 = green3 = blue3 = (red1 + green1 + blue1) / 3.0;
            break;
         }

         case 2: {                                                               //  black and white negative
            red3 = green3 = blue3 = 255.9 - (red1 + green1 + blue1) / 3.0;
            break;
         }

         case 3: {                                                               //  color positive (no change)
            exit_wthread();
            return 0;
         }

         case 4: {                                                               //  color negative
            red3 = 255.9 - red1;
            green3 = 255.9 - green1;
            blue3 = 255.9 - blue1;
            break;
         }

         case 5: {                                                               //  new sepia                          16.08
            brite = red1;
            if (green1 > brite) brite = green1;                                  //  max. color level
            if (blue1 > brite) brite = blue1;
            brite = 0.2 * brite + 0.2666 * (red1 + green1 + blue1);              //  brightness, 0.0 ... 255.9
            brite = brite * 0.003906;                                            //              0.0 ... 1.0

            ff1 = 1.0 - brite;                                                   //  sepia part, 1.0 ... 0.0
            ff2 = 1.0 - ff1;                                                     //  B & W part, 0.0 ... 1.0

            red3 = ff1 * 255.0 + ff2 * 256;                                      //  combine max. sepia with white
            green3 = ff1 * 150.0 + ff2 * 256;
            blue3 = ff1 * 46.0 + ff2 * 256;

            brite = 0.8333 * (brite + 0.2);                                      //  add brightness at low end
            red3 = red3 * brite;                                                 //  output = combined color * brightness
            green3 = green3 * brite;
            blue3 = blue3 * brite;

            break;
         }
      }
      
      if (red3 < 0) red3 = 0;                                                    //  prevent underflow/overlfow         15.05
      if (red3 > 255.9) red3 = 255.9;
      if (green3 < 0) green3 = 0;
      if (green3 > 255.9) green3 = 255.9;
      if (blue3 < 0) blue3 = 0;
      if (blue3 > 255.9) blue3 = 255.9;
      
      pix3 = PXMpix(E3pxm,px,py);

      if (sa_stat == 3 && dist < sa_blend) {                                     //  blend changes over blendwidth
         ff1 = sa_blendfunc(dist);                                               //  16.08
         ff2 = 1.0 - ff1;
         red3 = ff1 * red3 + ff2 * red1;
         green3 = ff1 * green3 + ff2 * green1;
         blue3 = ff1 * blue3 + ff2 * blue1;
      }
      
      if (blend < 1.0) {                                                         //  16.08
         ff1 = blend;
         ff2 = 1.0 - ff1;
         red3 = ff1 * red3 + ff2 * red1;
         green3 = ff1 * green3 + ff2 * green1;
         blue3 = ff1 * blue3 + ff2 * blue1;
      }

      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;
   }
   
   CEF->Fmods++;
   CEF->Fsaved = 0;

   exit_wthread();
   return 0;
}


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

//  Shift Colors menu function
//  Gradually shift selected RGB colors into other colors.

editfunc    EFshiftcolr;                                                         //  edit function data
float       shiftred = 0.5, shiftgreen = 0.5, shiftblue = 0.5;


//  menu function

void m_shift_colors(GtkWidget *, const char *menu)
{
   int    shiftcolr_dialog_event(zdialog* zd, const char *event);
   void * shiftcolr_thread(void *);

   F1_help_topic = "shift_colors";

   EFshiftcolr.menuname = menu;
   EFshiftcolr.menufunc = m_shift_colors;
   EFshiftcolr.funcname = "shift_colors";                                        //  function name
   EFshiftcolr.FprevReq = 1;                                                     //  use preview
   EFshiftcolr.Farea = 2;                                                        //  select area OK
   EFshiftcolr.Fscript = 1;                                                      //  scripting supported                15.10
   EFshiftcolr.threadfunc = shiftcolr_thread;                                    //  thread function

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

/***
                  Shift Colors

      red:    green =======[]======= blue
      green:   blue ==========[]==== red
      blue:     red ======[]======== green
      all:      all ========[]====== all

                    [reset] [done] [cancel]
***/

   zdialog *zd = zdialog_new(ZTX("Shift Colors"),Mwin,Breset,Bdone,Bcancel,null);
   EFshiftcolr.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=3");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=3");
   zdialog_add_widget(zd,"vbox","vb3","hb1",0,"homog|space=3|expand");
   zdialog_add_widget(zd,"vbox","vb4","hb1",0,"homog|space=3");

   zdialog_add_widget(zd,"label","labr","vb1",Bred);
   zdialog_add_widget(zd,"label","labr","vb1",Bgreen);
   zdialog_add_widget(zd,"label","labr","vb1",Bblue);
   zdialog_add_widget(zd,"label","labr","vb1",Ball);

   zdialog_add_widget(zd,"label","labg","vb2",Bgreen);
   zdialog_add_widget(zd,"label","labb","vb2",Bblue);
   zdialog_add_widget(zd,"label","labr","vb2",Bred);
   zdialog_add_widget(zd,"label","laba","vb2",Ball);

   zdialog_add_widget(zd,"hscale","red","vb3","0|1|0.001|0.5");
   zdialog_add_widget(zd,"hscale","green","vb3","0|1|0.001|0.5");
   zdialog_add_widget(zd,"hscale","blue","vb3","0|1|0.001|0.5");
   zdialog_add_widget(zd,"hscale","all","vb3","0|1|0.001|0.5");

   zdialog_add_widget(zd,"label","labb","vb4",Bblue);
   zdialog_add_widget(zd,"label","labr","vb4",Bred);
   zdialog_add_widget(zd,"label","labg","vb4",Bgreen);
   zdialog_add_widget(zd,"label","laba","vb4",Ball);

   zdialog_rescale(zd,"red",0,0.5,1);                                            //  15.01
   zdialog_rescale(zd,"green",0,0.5,1);
   zdialog_rescale(zd,"blue",0,0.5,1);
   zdialog_rescale(zd,"all",0,0.5,1);
   
   shiftred = shiftgreen = shiftblue = 0.5;                                      //  neutral values                     15.05

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


//  shift color dialog event and completion function

int shiftcolr_dialog_event(zdialog *zd, const char *event)
{
   float    shiftall;

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 2;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel                    15.07
   
   if (strmatch(event,"fullsize")) {                                             //  from select area
      if (! CEF->Fpreview) return 1;
      edit_fullsize();
      signal_thread();
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  reset 
         zd->zstat = 0;                                                          //  keep dialog active
         zdialog_stuff(zd,"red",0.5);
         zdialog_stuff(zd,"green",0.5);
         zdialog_stuff(zd,"blue",0.5);
         zdialog_stuff(zd,"all",0.5);
         edit_reset();
         return 1;
      }
      if (zd->zstat == 2) {                                                      //  done
         edit_fullsize();                                                        //  get full size image
         signal_thread();
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }
   
   if (strmatch(event,"focus")) return 1;

   if (strmatch(event,"all")) {
      zdialog_fetch(zd,"all",shiftall);
      shiftred = shiftgreen = shiftblue = shiftall;
      zdialog_stuff(zd,"red",shiftred);
      zdialog_stuff(zd,"green",shiftgreen);
      zdialog_stuff(zd,"blue",shiftblue);
   }
   
   zdialog_fetch(zd,"red",shiftred);
   zdialog_fetch(zd,"green",shiftgreen);
   zdialog_fetch(zd,"blue",shiftblue);

   if (strstr("red green blue all blendwidth apply",event))                      //  trigger update thread
      signal_thread();

   return 1;
}


//  thread function - launch multiple working threads to update image

void * shiftcolr_thread(void *)
{
   void  * shiftcolr_wthread(void *arg);                                         //  worker thread

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

      for (int ii = 0; ii < NWT; ii++)                                           //  start worker threads
         start_wthread(shiftcolr_wthread,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

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

   return 0;                                                                     //  not executed, stop warning
}


void * shiftcolr_wthread(void *arg)                                              //  worker thread function
{
   int         index = *((int *) (arg));
   int         px, py, ii, dist = 0;
   float       *pix1, *pix3;
   float       red1, green1, blue1, red3, green3, blue3;
   float       lossR, lossG, lossB, gainR, gainG, gainB;
   float       shift, move, maxrgb, f1, f2;

   for (py = index; py < E3pxm->hh; py += NWT)                                   //  loop all image pixels
   for (px = 0; px < E3pxm->ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3pxm->ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

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

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

      lossR = lossG = lossB = 0;
      gainR = gainG = gainB = 0;

      shift = shiftred;                                                          //  red shift: green <-- red --> blue
      if (shift <= 0.5) {                                                        //  0.0 ... 0.5
         move = red1 * 2.0 * (0.5 - shift);
         lossR += move;
         gainG += move;
      }
      if (shift > 0.5) {                                                         //  0.5 ... 1.0
         move = red1 * 2.0 * (shift - 0.5);
         lossR += move;
         gainB += move;
      }

      shift = shiftgreen;                                                        //  green shift: blue <-- green --> red
      if (shift <= 0.5) {                                                        //  0.0 ... 0.5
         move = green1 * 2.0 * (0.5 - shift);
         lossG += move;
         gainB += move;
      }
      if (shift > 0.5) {                                                         //  0.5 ... 1.0
         move = green1 * 2.0 * (shift - 0.5);
         lossG += move;
         gainR += move;
      }

      shift = shiftblue;                                                         //  blue shift: red <-- blue --> green
      if (shift <= 0.5) {                                                        //  0.0 ... 0.5
         move = blue1 * 2.0 * (0.5 - shift);
         lossB += move;
         gainR += move;
      }
      if (shift > 0.5) {                                                         //  0.5 ... 1.0
         move = blue1 * 2.0 * (shift - 0.5);
         lossB += move;
         gainG += move;
      }

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

      red3 = red1 - lossR + gainR;
      green3 = green1 - lossG + gainG;
      blue3 = blue1 - lossB + gainB;

      maxrgb = red3;                                                             //  find max. new RGB color
      if (green3 > maxrgb) maxrgb = green3;
      if (blue3 > maxrgb) maxrgb = blue3;

      if (maxrgb > 255.9) {                                                      //  if too big, rescale all colors
         red3 = red3 * 255.9 / maxrgb;
         green3 = green3 * 255.9 / maxrgb;
         blue3 = blue3 * 255.9 / maxrgb;
      }

      if (sa_stat == 3 && dist < sa_blend) {                                     //  select area is active,
///      f1 = 1.0 * dist / sa_blend;                                             //    blend changes over sa_blend
         f1 = sa_blendfunc(dist);                                                //  16.08
         f2 = 1.0 - f1;
         red3 = f1 * red3 + f2 * red1;
         green3 = f1 * green3 + f2 * green1;
         blue3 = f1 * blue3 + f2 * blue1;
      }

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

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


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

//  image color saturation adjustment

float       colorsat;
editfunc    EFcolorsat;


//  menu function

void m_colorsat(GtkWidget *, cchar *)                                            //  16.01
{
   int    colorsat_dialog_event(zdialog *zd, cchar *event);
   void * colorsat_thread(void *);

   cchar  *colsatmess = ZTX("Color Saturation");

   F1_help_topic = "color_saturation";

   EFcolorsat.menufunc = m_colorsat;
   EFcolorsat.funcname = "color-saturation";
   EFcolorsat.FprevReq = 1;                                                      //  use preview
   EFcolorsat.Farea = 2;                                                         //  select area usable
   EFcolorsat.FusePL = 1;                                                        //  use with paint/lever edits OK
   EFcolorsat.threadfunc = colorsat_thread;                                      //  thread function
   if (! edit_setup(EFcolorsat)) return;                                         //  setup edit

   zdialog *zd = zdialog_new(colsatmess,Mwin,Bdone,Bcancel,null);
   EFcolorsat.zd = zd;
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",colsatmess,"space=3");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"hscale","colorsat","hb2","-1.0|1.0|0.01|0","expand|space=5");

   colorsat = 1.0;

   zdialog_resize(zd,250,0);
   zdialog_run(zd,colorsat_dialog_event,"save");                                 //  run dialog, parallel

   return;
}


//  dialog event and completion callback function

int colorsat_dialog_event(zdialog *zd, cchar *event)
{

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

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  done
         edit_fullsize();                                                        //  get full size image
         signal_thread();
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   if (strmatch(event,"undo")) edit_undo();
   if (strmatch(event,"redo")) edit_redo();
   if (strmatch(event,"blendwidth")) signal_thread();

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

   return 0;
}


//  image color saturation thread function

void * colorsat_thread(void *)
{
   int         ii, px, py, dist = 0;
   float       *pix1, *pix3;
   float       red1, green1, blue1;
   float       red3, green3, blue3;
   float       pixbrite, maxrgb, F1, F2;

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

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

         pix1 = PXMpix(E1pxm,px,py);                                             //  input pixel
         pix3 = PXMpix(E3pxm,px,py);                                             //  output pixel
         
         red1 = pix1[0];
         green1 = pix1[1];
         blue1 = pix1[2];

         pixbrite = 0.333 * (red1 + green1 + blue1);                             //  pixel brightness, 0 to 255.9
         red3 = red1 + colorsat * (red1 - pixbrite);                             //  colorsat: -1 ... +1
         green3 = green1 + colorsat * (green1 - pixbrite);
         blue3 = blue1 + colorsat * (blue1 - pixbrite);

         if (red3 < 0) red3 = 0;                                                 //  stop underflow
         if (green3 < 0) green3 = 0;
         if (blue3 < 0) blue3 = 0;

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

         if (sa_stat == 3 && dist < sa_blend) {                                  //  select area edge blending
///         F2 = 1.0 * dist / sa_blend;
            F2 = sa_blendfunc(dist);                                             //  16.08
            F1 = 1.0 - F2;
            red3 = F2 * red3 + F1 * red1;
            green3 = F2 * green3 + F1 * green1;
            blue3 = F2 * blue3 + F1 * blue1;
         }

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

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

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


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

//  Adjust RGB menu function
//  Adjust Brightness, contrast, and color levels using RGB or CMY colors

editfunc    EF_RGB;                                                              //  edit function data
float       RGB_inputs[8];


//  menu function

void m_adjust_RGB(GtkWidget *, const char *menu)
{
   int    RGB_dialog_event(zdialog *zd, cchar *event);
   void * RGB_thread(void *);

   F1_help_topic = "adjust_RGB_CMY";

   EF_RGB.menuname = menu;
   EF_RGB.menufunc = m_adjust_RGB;
   EF_RGB.funcname = "adjust_RGB";                                               //  function name
   EF_RGB.FprevReq = 1;                                                          //  use preview
   EF_RGB.Farea = 2;                                                             //  select area usable
   EF_RGB.Frestart = 1;                                                          //  allow restart
   EF_RGB.Fscript = 1;                                                           //  scripting supported                15.10
   EF_RGB.threadfunc = RGB_thread;                                               //  thread function
   if (! edit_setup(EF_RGB)) return;                                             //  setup edit

/***
    _______________________________________________________
   |                                                       |
   |  +Brightness      =====[]=====  Contrast =====[]===== |
   |  +Red -Cyan       =====[]=====     Red   =====[]===== |
   |  +Green -Magenta  =====[]=====   Green   =====[]===== |
   |  +Blue -Yellow    =====[]=====    Blue   =====[]===== |
   |                                                       |
   |                              [reset] [done] [cancel]  |
   |_______________________________________________________|

***/

   zdialog *zd = zdialog_new("Adjust RGB",Mwin,Breset,Bdone,Bcancel,null); 
   EF_RGB.zd = zd;

   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb2",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb2",0,"homog|expand|space=5");
   zdialog_add_widget(zd,"label","space","hb2",0,"space=8");
   zdialog_add_widget(zd,"vbox","vb3","hb2",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb4","hb2",0,"homog|expand|space=5");
   zdialog_add_widget(zd,"label","labBriteDens","vb1",ZTX("+Brightness"));
   zdialog_add_widget(zd,"label","labRedDens","vb1",ZTX("+Red -Cyan"));
   zdialog_add_widget(zd,"label","labGreenDens","vb1",ZTX("+Green -Magenta"));
   zdialog_add_widget(zd,"label","labBlueDens","vb1",ZTX("+Blue -Yellow"));
   zdialog_add_widget(zd,"hscale","BriteDens","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale","RedDens","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale","GreenDens","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale","BlueDens","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"label","labContrast","vb3",Bcontrast);
   zdialog_add_widget(zd,"label","labRedCon","vb3",ZTX("Red"));
   zdialog_add_widget(zd,"label","labGreenCon","vb3",ZTX("Green"));
   zdialog_add_widget(zd,"label","labBlueCon","vb3",ZTX("Blue"));
   zdialog_add_widget(zd,"hscale","Contrast","vb4","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale","RedCon","vb4","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale","GreenCon","vb4","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale","BlueCon","vb4","-1|+1|0.001|0","expand");

   zdialog_rescale(zd,"BriteDens",-1,0,+1);                                      //  15.01
   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,500,0);
   zdialog_restore_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, cchar *event)                                  //  RGB dialog event function
{
   int      mod = 0;

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 2;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel                    15.07
   
   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();
      signal_thread();
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  reset
         zd->zstat = 0;                                                          //  keep dialog active
         RGB_inputs[0] = 0;
         RGB_inputs[1] = 0;
         RGB_inputs[2] = 0;
         RGB_inputs[3] = 0;
         RGB_inputs[4] = 0;
         RGB_inputs[5] = 0;
         RGB_inputs[6] = 0;
         RGB_inputs[7] = 0;
         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);
         mod++;
      }
      else if (zd->zstat == 2) {                                                 //  done
         edit_fullsize();                                                        //  get full size image
         signal_thread();
         edit_done(0);                                                           //  commit edit
         return 1;
      }
      else {
         edit_cancel(0);                                                         //  discard edit
         return 1;
      }
   }

   if (strmatch("focus",event)) return 1;
   
   zdialog_fetch(zd,"BriteDens",RGB_inputs[0]);                                  //  get all inputs
   zdialog_fetch(zd,"RedDens",RGB_inputs[1]);
   zdialog_fetch(zd,"GreenDens",RGB_inputs[2]);
   zdialog_fetch(zd,"BlueDens",RGB_inputs[3]);
   zdialog_fetch(zd,"Contrast",RGB_inputs[4]);
   zdialog_fetch(zd,"RedCon",RGB_inputs[5]);
   zdialog_fetch(zd,"GreenCon",RGB_inputs[6]);
   zdialog_fetch(zd,"BlueCon",RGB_inputs[7]);

   if (RGB_inputs[0]) mod++;
   if (RGB_inputs[1]) mod++;
   if (RGB_inputs[2]) mod++;
   if (RGB_inputs[3]) mod++;
   if (RGB_inputs[4]) mod++;
   if (RGB_inputs[5]) mod++;
   if (RGB_inputs[6]) mod++;
   if (RGB_inputs[7]) mod++;
   
   if (mod) signal_thread();                                                     //  trigger update thread
   return 1;
}


//  thread function - multiple working threads to update image

void * RGB_thread(void *)
{
   void  * RGB_wthread(void *arg);                                               //  worker thread

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

      if (sa_stat == 3) Fbusy_goal = sa_Npixel;                                  //  set up progress monitor
      else  Fbusy_goal = E3pxm->ww * E3pxm->hh;
      Fbusy_done = 0;

      for (int ii = 0; ii < NWT; ii++)                                           //  start worker threads
         start_wthread(RGB_wthread,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

      Fbusy_goal = Fbusy_done = 0;
      CEF->Fmods++;                                                              //  image modified
      CEF->Fsaved = 0;
      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop warning
}


//  worker thread function

void * RGB_wthread(void *arg)                                                    //  overhauled
{
   float    R1, G1, B1, R3, G3, B3;
   float    briA, briR, briG, briB;
   float    conA, conR, conG, conB;
   float    R, G, B;

   int      index = *((int *) (arg));
   int      px, py, ii, dist = 0;
   float    *pix1, *pix3;
   float    cmax, F, f1, f2;

   briA = RGB_inputs[0];                                                         //  color brightness inputs, -1 to +1
   briR = RGB_inputs[1];
   briG = RGB_inputs[2];
   briB = RGB_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                 15.05
   B += 1;

   briR = R;                                                                     //  final color brightness factors
   briG = G;
   briB = B;

   conA = RGB_inputs[4];                                                         //  contrast inputs, -1 to +1
   conR = RGB_inputs[5];
   conG = RGB_inputs[6];
   conB = RGB_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;

   for (py = index; py < E3pxm->hh; py += NWT)                                   //  loop all image pixels
   for (px = 0; px < E3pxm->ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3pxm->ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside area
      }

      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 = R1 * briR;                                                            //  apply color brightness factors
      G3 = G1 * briG;
      B3 = B1 * briB;

      R3 = conR * (R3 - 128) + 128;                                              //  apply contrast factors
      G3 = conG * (G3 - 128) + 128;
      B3 = conB * (B3 - 128) + 128;

      if (R3 < 0) R3 = 0;                                                        //  stop underflow
      if (G3 < 0) G3 = 0;
      if (B3 < 0) B3 = 0;

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

      if (sa_stat == 3 && dist < sa_blend) {                                     //  select area is active,
///      f1 = 1.0 * dist / sa_blend;                                             //    blend changes over sa_blend
         f1 = sa_blendfunc(dist);                                                //  16.08
         f2 = 1.0 - f1;
         R3 = f1 * R3 + f2 * R1;
         G3 = f1 * G3 + f2 * G1;
         B3 = f1 * B3 + f2 * B1;
      }

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

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


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

//  HSL color menu function
//  Adjust colors using the HSL (hue/saturation/lightness) color model

namespace HSL_names
{
   GtkWidget   *RGBframe, *RGBcolor;
   GtkWidget   *Hframe, *Hscale;
   editfunc    EFHSL;                                                            //  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%
}

void HSLtoRGB(float H, float S, float L, float &R, float &G, float &B);
void RGBtoHSL(float R, float G, float B, float &H, float &S, float &L);


//  menu function

void m_adjust_HSL(GtkWidget *, const char *)                                     //  overhauled                         16.06
{
   using namespace 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, cchar *event);
   void   HSL_mousefunc();
   void * HSL_thread(void *);
   
   F1_help_topic = "adjust_HSL";

   EFHSL.menufunc = m_adjust_HSL;
   EFHSL.funcname = "adjust_HSL";                                                //  function name
   EFHSL.FprevReq = 1;                                                           //  use preview
   EFHSL.Farea = 2;                                                              //  select area usable
   EFHSL.Frestart = 1;                                                           //  allow restart
   EFHSL.FusePL = 1;                                                             //  use with paint/lever edits OK
   EFHSL.mousefunc = HSL_mousefunc;                                              //  mouse function
   EFHSL.threadfunc = HSL_thread;                                                //  thread function
   if (! edit_setup(EFHSL)) 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] [done] [cancel]  |
      |___________________________________________________|

***/

   zdialog *zd = zdialog_new("Adjust HSL",Mwin,Breset,Bdone,Bcancel,null);
   EFHSL.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"label","labmatch","hb1",ZTX("Input color to match and adjust:"),"space=5");
   zdialog_add_widget(zd,"colorbutt","matchRGB","hb1","0|0|0");
   zdialog_add_ttip(zd,"matchRGB",ZTX("shift+click on image to select color"));
   
   zdialog_add_widget(zd,"hbox","hbmu","dialog");
   zdialog_add_widget(zd,"label","labmu","hbmu",ZTX("Match using:"),"space=5");
   zdialog_add_widget(zd,"check","Huse","hbmu",ZTX("Hue"),"space=3");
   zdialog_add_widget(zd,"check","Suse","hbmu",ZTX("Saturation"),"space=3");
   zdialog_add_widget(zd,"check","Luse","hbmu",ZTX("Lightness"),"space=3");
   
   zdialog_add_widget(zd,"hbox","hbmatch","dialog");
   zdialog_add_widget(zd,"label","labmatch","hbmatch",ZTX("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",ZTX("Output Color"));

   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb2",0,"homog|space=0");
   zdialog_add_widget(zd,"vbox","vb2","hb2",0,"homog|expand|space=0");

   zdialog_add_widget(zd,"frame","RGBframe","vb1",0,"space=1");                  //  drawing area for RGB color
   RGBframe = zdialog_widget(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_widget(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",ZTX("Color Hue"));
   zdialog_add_widget(zd,"check","Sout","vb1",ZTX("Saturation"));
   zdialog_add_widget(zd,"check","Lout","vb1",ZTX("Lightness"));
   zdialog_add_widget(zd,"label","labadjust","vb1",ZTX("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 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 HSL_names;

   int      px, ww, hh;
   float    H, R, G, B;

   ww = gtk_widget_get_allocated_width(drawarea);                                //  drawing area size
   hh = gtk_widget_get_allocated_height(drawarea);
   
   for (px = 0; px < ww; px++)                                                   //  paint hue color scale
   {
      H = 360 * px / ww;
      HSLtoRGB(H,Sc,Lc,R,G,B);                                                   //  S and L are current settings
      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, cchar *event)                                  //  HSL dialog event function
{
   using namespace HSL_names;

   void   HSL_mousefunc();

   int      mod = 0;

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 2;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel
   
   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();
      signal_thread();
      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);
         mod = 1;
      }
      else if (zd->zstat == 2) {                                                 //  done
         edit_fullsize();                                                        //  get full size image
         signal_thread();
         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("blendwidth",event)) mod = 1;                                    //  area blend width changed
   
   if (mod) {
      gtk_widget_queue_draw(RGBcolor);                                           //  draw current RGB color
      gtk_widget_queue_draw(Hscale);                                             //  draw HSL scale with new S and L
      signal_thread();                                                           //  trigger update thread
   }

   return 1;
}


//  mouse function
//  click on image to set the color to match and change

void HSL_mousefunc()
{
   using namespace HSL_names;

   char        color[20];
   float       *pix1;
   float       f256 = 1.0 / 256.0;
   zdialog     *zd = EFHSL.zd;
   
   if (LMclick && KBshiftkey)                                                    //  shift + left mouse click
   {
      LMclick = 0;
      pix1 = PXMpix(E1pxm,Mxclick,Myclick);                                      //  pick match color from image
      Rm = pix1[0];
      Gm = pix1[1];
      Bm = pix1[2];
      snprintf(color,19,"%.0f|%.0f|%.0f",Rm,Gm,Bm);
      if (zd) zdialog_stuff(zd,"matchRGB",color);

      Rm *= f256;
      Gm *= f256;
      Bm *= f256;
      RGBtoHSL(Rm,Gm,Bm,Hm,Sm,Lm);                                               //  set HSL color to match
   }

   return;
}


//  thread function - multiple working threads to update image

void * HSL_thread(void *)
{
   using namespace HSL_names;

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

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

      for (int ii = 0; ii < NWT; ii++)                                           //  start worker threads
         start_wthread(HSL_wthread,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

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

   return 0;                                                                     //  not executed, stop warning
}


//  worker thread function

void * HSL_wthread(void *arg)
{
   using namespace HSL_names;
   
   int      index = *((int *) (arg));
   int      px, py, ii, dist = 0;
   float    *pix1, *pix3;
   float    R1, G1, B1, R3, G3, B3;
   float    H1, S1, L1, H3, S3, L3;
   float    dH, dS, dL, match;
   float    a1, a2, f1, f2;
   float    f256 = 1.0 / 256.0;

   for (py = index; py < E3pxm->hh; py += NWT)                                   //  loop all image pixels
   for (px = 0; px < E3pxm->ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3pxm->ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside area
      }

      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      
      R1 = f256 * pix1[0];                                                       //  input pixel RGB
      G1 = f256 * pix1[1];
      B1 = f256 * pix1[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          16.06
      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,R3,G3,B3);
      
      if (sa_stat == 3 && dist < sa_blend) {                                     //  select area is active,
///      f1 = 1.0 * dist / sa_blend;                                             //    blend changes over sa_blend
         f1 = sa_blendfunc(dist);                                                //  16.08
         f2 = 1.0 - f1;
         R3 = f1 * R3 + f2 * R1;
         G3 = f1 * G3 + f2 * G1;
         B3 = f1 * B3 + f2 * B1;
      }
      
      pix3[0] = 255.0 * R3;
      pix3[1] = 255.0 * G3;
      pix3[2] = 255.0 * B3;
   }

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


//  HSL to RGB converter (Wikipedia)
//  H = 0-360 deg.  S = 0-1   L = 0-1
//  output RGB values = 0-1

void HSLtoRGB(float H, float S, float L, float &R, float &G, float &B)
{
   float    C, X, M;
   float    h1, h2;
   
   h1 = H / 60;
   h2 = h1 - 2 * int(h1/2);
   
   C = (1 - fabsf(2*L-1)) * S;
   X = C * (1 - fabsf(h2-1));
   M = L - C/2;
   
   if (H < 60) {
      R = C;
      G = X;
      B = 0;
   }
   
   else if (H < 120) {
      R = X;
      G = C;
      B = 0;
   }
   
   else if (H < 180) {
      R = 0;
      G = C;
      B = X;
   }
   
   else if (H < 240) {
      R = 0;
      G = X;
      B = C;
   }
   
   else if (H < 300) {
      R = X;
      G = 0;
      B = C;
   }
   
   else {
      R = C;
      G = 0;
      B = X;
   }
   
   R = R + M;
   G = G + M;
   B = B + M;
   
   return;
}


//  RGB to HSL converter 
//  input RGB values 0-1
//  outputs: H = 0-360 deg.  S = 0-1   L = 0-1

void RGBtoHSL(float R, float G, float B, float &H, float &S, float &L)           //  15.12
{
   float    max, min, D;

   max = R;
   if (G > max) max = G;
   if (B > max) max = B;
   
   min = R;
   if (G < min) min = G;
   if (B < min) min = B;
   
   D = max - min;
   
   L = 0.5 * (max + min);
   
   if (D < 0.004) {
      H = S = 0;
      return;
   }
   
   if (L > 0.5)
      S = D / (2 - max - min);
   else
      S = D / (max + min);
   
   if (max == R) 
      H = (G - B) / D;
   else if (max == G)
      H = 2 + (B - R) / D;
   else
      H = 4 + (R - G) / D;

   H = H * 60;
   if (H < 0) H += 360;

   return;
}


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

//  ramp brightness across image, vertical and horizontal gradients

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

void m_ramp_brightness(GtkWidget *, cchar *menu)                                 //  upgrade for 4 colors
{
   int    brramp_dialog_event(zdialog* zd, cchar *event);
   void   brramp_curvedit(int spc);
   void * brramp_thread(void *);
   void   brramp_init();

   F1_help_topic = "ramp_brightness";

   EFbrramp.menuname = menu;
   EFbrramp.menufunc = m_ramp_brightness;
   EFbrramp.funcname = "bright-ramp";
   EFbrramp.FprevReq = 1;                                                        //  use preview
   EFbrramp.Farea = 2;                                                           //  select area usable
   EFbrramp.Fscript = 1;                                                         //  scripting supported                15.10
   EFbrramp.threadfunc = brramp_thread;
   if (! edit_setup(EFbrramp)) return;                                           //  setup edit

/***
          _________________________________________
         |                                         |
         |                                         |
         |           curve edit area               |
         |                                         |
         |_________________________________________|

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

***/

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

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

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

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

   brramp_init();                                                                //  set up initial flat curves

   zdialog_stuff(zd,"all",1);                                                    //  stuff default selection, all

   zdialog_resize(zd,260,350);
   zdialog_run(zd,brramp_dialog_event,"save");                                   //  run dialog - parallel

   return;
}


//  initialize all curves to flat

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

   sd->Nspc = 8;                                                                 //  8 curves
   brramp_spc = 0;                                                               //  current curves: all (0-1)

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

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

   return;
}


//  dialog event and completion callback function

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

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  apply and quit
   if (strmatch(event,"enter")) zd->zstat = 1;
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  cancel
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   if (strmatch(event,"apply")) Fupdate++;                                       //  from script                        15.10
   
   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();
      signal_thread();
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  done
         edit_fullsize();                                                        //  get full size image
         signal_thread();
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

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

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

      for (ii = 0; ii < 8; ii++)                                                 //  set active curves                  15.10
         sd->fact[ii] = 0;
      ii = strmatchV(event,"all","red","green","blue",null);                     //  1-4
      brramp_spc = ii = 2 * (ii-1);                                              //  0/1, 2/3, 4/5, 6/7
      sd->fact[ii] = sd->fact[ii+1] = 1;

      Fupdate++;
   }

   if (strmatch(event,"loadcurves"))                                             //  load 8 saved curves from file
   {
      nn = splcurve_load(sd);
      if (nn != 1) return 0;
      Fupdate++;
   }

   if (strmatch(event,"savecurves"))                                             //  save 8 curves to file
      splcurve_save(sd);

   if (Fupdate)                                                                  //  curve has changed
   {
      gtk_widget_queue_draw(sd->drawarea);                                       //  draw curve
      signal_thread();                                                           //  trigger image update
   }

   return 1;
}


//  this function is called when a curve is edited

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

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

   signal_thread();
   return;
}


//  brramp thread function

void * brramp_thread(void *arg)
{
   void * brramp_wthread(void *);

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

      for (int ii = 0; ii < NWT; ii++)                                           //  start worker threads
         start_wthread(brramp_wthread,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

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

      Fpaint2();                                                                 //  update window
   }

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


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

   if (sa_stat == 3) {                                                           //  if select area active, ramp
      spanw = sa_maxx - sa_minx;                                                 //    brightness over enclosing rectangle
      spanh = sa_maxy - sa_miny;
   }
   else {
      spanw = E3pxm->ww;                                                         //  else over entire image
      spanh = E3pxm->hh;
   }

   for (py3 = index; py3 < E3pxm->hh; py3 += NWT)                                //  loop output pixels
   for (px3 = 0; px3 < E3pxm->ww; px3++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py3 * E3pxm->ww + px3;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel outside area
         dispx = (px3 - sa_minx) / spanw;
         dispy = (py3 - sa_miny) / spanh;
      }
      else {
         dispx = px3 / spanw;                                                    //  left > right = 0 to 1
         dispy = py3 / spanh;                                                    //  top > bottom = 0 to 1
      }

      kk = 1000 * dispx;                                                         //  conv. dispx 0-1 to 0-1000
      if (kk > 999) kk = 999;

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

      kk = 1000 * dispy;                                                         //  conv. dispy
      if (kk > 999) kk = 999;

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

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

      pix1 = PXMpix(E1pxm,px3,py3);                                              //  input pixel
      red1 = pix1[0];
      green1 = pix1[1];
      blue1 = pix1[2];

      red3 = red1 * tramp[0];                                                    //  change R/G/B
      green3 = green1 * tramp[1];
      blue3 = blue1 * tramp[2];

      maxrgb = red3;
      if (green3 > maxrgb) maxrgb = green3;
      if (blue3 > maxrgb) maxrgb = blue3;

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

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

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

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


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

//  Color ramp function. Revise color in selected image areas.
//  Show RGB values for 1-9 pixels selected with mouse-clicks.
//  Revise RGB values for the selected pixels and then revise all other
//  pixels to match, using a distance-weighted average of the selected pixels.

void  ramp_color_dialog();
void  ramp_color_mousefunc();
void  ramp_color_stuff();
void * ramp_color_thread(void *);

int      ramp_color_pixel[9][2];                                                 //  last 9 pixel locations clicked
float    ramp_color_val1[9][3];                                                  //  original RGB values, 0 to 255.9
float    ramp_color_val3[9][3];                                                  //  revised RGB values, 0 to 255.9
int      ramp_color_metric = 1;                                                  //  1/2/3 = /RGB/EV/OD
int      ramp_color_delta = 0;                                                   //  flag, delta mode
int      ramp_color_npix;                                                        //  no. of selected pixels
double   ramp_color_time;                                                        //  last click time
int      ramp_color_blend;                                                       //  blend factor, 1 to 100

editfunc EFrgbrevise;                                                            //  edit function parameters

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


//  menu function

void m_ramp_color(GtkWidget *, cchar *menu)
{
   int   ramp_color_event(zdialog *zd, cchar *event);

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

   F1_help_topic = "ramp_color";

   EFrgbrevise.menufunc = m_ramp_color;
   EFrgbrevise.funcname = "ramp_color";                                          //  setup edit function
   EFrgbrevise.threadfunc = ramp_color_thread;
   EFrgbrevise.mousefunc = ramp_color_mousefunc;
   EFrgbrevise.Farea = 1;
   if (! edit_setup(EFrgbrevise)) return;

/***

      Click image to select pixels.
      [x] delta
      Metric: (o) RGB  (o) EV  (o) OD
      Pixel          Red      Green    Blue
      A xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v]                                   //  spin buttons for RGB/EV/OD values
      B xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v]
      C xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v]
      D xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v]
      E xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v]
      F xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v]
      G xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v]
      H xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v]
      I xxxx xxxx   [xxxx|v] [xxxx|v] [xxxx|v]
      Blend  ==============[]================

                     [reset] [done] [cancel]
***/

   zd = zdialog_new(ZTX("Color Ramp"),Mwin,Breset,Bdone,Bcancel,null);
   EFrgbrevise.zd = zd;

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

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

   zdialog_add_widget(zd,"hbox","hbmetr","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labmetr","hbmetr",ZTX("Metric:"),"space=5");
   zdialog_add_widget(zd,"radio","radRGB","hbmetr","RGB","space=3");
   zdialog_add_widget(zd,"radio","radEV","hbmetr","EV","space=3");
   zdialog_add_widget(zd,"radio","radOD","hbmetr","OD","space=3");

   if (ramp_color_metric == 1) zdialog_stuff(zd,"radRGB",1);                     //  set current metric
   if (ramp_color_metric == 2) zdialog_stuff(zd,"radEV",1);
   if (ramp_color_metric == 3) zdialog_stuff(zd,"radOD",1);

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

   char hbpixx[8] = "hbpixx", pixx[8] = "pixx";

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

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

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

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

   for (int ii = 1; ii < 10; ii++) {
      hbdatx[5] = '0' + ii;
      redx[3] = '0' + ii;
      greenx[5] = '0' + ii;
      bluex[4] = '0' + ii;
      zdialog_add_widget(zd,"hbox",hbdatx,"vbdat");                              //  add hbox "hbdat1" to "hbdat9"
      zdialog_add_widget(zd,"spin",redx,hbdatx,limits,"space=3");                //  add spin buttons "red1" to "red9" etc.
      zdialog_add_widget(zd,"spin",greenx,hbdatx,limits,"space=3");
      zdialog_add_widget(zd,"spin",bluex,hbdatx,limits,"space=3");
   }

   zdialog_add_widget(zd,"hbox","hbsoft","dialog","space=5");
   zdialog_add_widget(zd,"label","labsoft","hbsoft",ZTX("Blend"),"space=3");
   zdialog_add_widget(zd,"hscale","blend","hbsoft","0|100|1|20","space=5|expand");
   zdialog_add_widget(zd,"label","softval","hbsoft","20");

   if (strmatch(menu,"restart"))                                                 //  stuff current pixels if restart
      ramp_color_stuff();
   else {
      ramp_color_blend = 20;                                                     //  default slider value
      ramp_color_npix = 0;                                                       //  no pixels selected yet
   }

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

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


//  dialog event function

int ramp_color_event(zdialog *zd, cchar *event)
{
   int         button;
   int         ii, jj;
   char        text[8];
   float       val1, val3;

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 2;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel                    15.07
   
   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  Reset
         zd->zstat = 0;                                                          //  keep dialog active
         ramp_color_npix = 0;                                                    //  no pixels selected yet
         ramp_color_stuff();                                                     //  clear dialog
         edit_reset();                                                           //  reset edits
         return 1;
      }

      if (zd->zstat == 2) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      EFrgbrevise.zd = 0;
      erase_toptext(101);                                                        //  erase pixel labels from image
      return 1;
   }

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

   if (strmatch(event,"blend")) {                                                //  new blend factor
      zdialog_fetch(zd,"blend",ramp_color_blend);
      snprintf(text,8,"%d",ramp_color_blend);                                    //  numeric feedback
      zdialog_stuff(zd,"softval",text);
      signal_thread();
      return 1;
   }

   if (strmatchN(event,"rad",3))                                                 //  metric was changed
   {
      if (strmatch(event,"radRGB")) {                                            //  RGB
         zdialog_fetch(zd,event,button);
         if (button) ramp_color_metric = 1;
      }

      if (strmatch(event,"radEV")) {                                             //  EV
         zdialog_fetch(zd,event,button);
         if (button) ramp_color_metric = 2;
      }

      if (strmatch(event,"radOD")) {                                             //  OD
         zdialog_fetch(zd,event,button);
         if (button) ramp_color_metric = 3;
      }

      if (button) {                                                              //  restart with new limits
         edit_cancel(0);
         m_ramp_color(0,"restart");
      }

      return 1;
   }

   if (strmatch(event,"delta")) {                                                //  change absolute/delta mode
      zdialog_fetch(zd,"delta",ramp_color_delta);
      edit_cancel(0);
      m_ramp_color(0,"restart");
      return 1;
   }

   ii = -1;                                                                      //  no RGB change yet

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

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

   if (strmatchN(event,"blue",4)) {                                              //  blue1 - blue9
      ii = event[4] - '1';
      jj = 2;
   }

   if (ii >= 0 && ii < 9)                                                        //  RGB value was revised
   {
      val1 = ramp_color_val1[ii][jj];                                            //  original pixel RGB value
      if (ramp_color_metric == 2) val1 = RGB2EV(val1);                           //  convert to EV or OD units
      if (ramp_color_metric == 3) val1 = RGB2OD(val1);

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

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

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

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

      ramp_color_val3[ii][jj] = val3;                                            //  new RGB value for pixel

      if (ramp_color_metric == 2) val3 = RGB2EV(val3);                           //  convert RGB to EV/OD units
      if (ramp_color_metric == 3) val3 = RGB2OD(val3);

      if (ramp_color_delta) val3 -= val1;                                        //  absolute back to relative
      zdialog_stuff(zd,event,val3);                                              //  limited value back to dialog

      signal_thread();                                                           //  signal thread to update image
   }

   return 1;
}


//  mouse function

void ramp_color_mousefunc()                                                      //  mouse function
{
   int         ii;
   float       *pix3;

   if (! LMclick) return;
   LMclick = 0;

   ramp_color_time = get_seconds();                                              //  mark time of pixel click

   if (ramp_color_npix == 9) {                                                   //  if table is full (9 entries)
      for (ii = 1; ii < 9; ii++) {                                               //    move positions 1-8 up
         ramp_color_pixel[ii-1][0] = ramp_color_pixel[ii][0];                    //      to fill positions 0-7
         ramp_color_pixel[ii-1][1] = ramp_color_pixel[ii][1];
      }
      ramp_color_npix = 8;                                                       //  count is now 8 entries
   }

   ii = ramp_color_npix;                                                         //  next table position to fill
   ramp_color_pixel[ii][0] = Mxclick;                                            //  newest pixel
   ramp_color_pixel[ii][1] = Myclick;

   pix3 = PXMpix(E3pxm,Mxclick,Myclick);                                         //  get initial RGB values from
   ramp_color_val1[ii][0] = ramp_color_val3[ii][0] = pix3[0];                    //    modified image E3
   ramp_color_val1[ii][1] = ramp_color_val3[ii][1] = pix3[1];
   ramp_color_val1[ii][2] = ramp_color_val3[ii][2] = pix3[2];

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


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

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

   int         px, py;
   float       red1, green1, blue1, red3, green3, blue3;
   char        text[100], pixx[8] = "pixx";
   char        redx[8] = "redx", greenx[8] = "greenx", bluex[8] = "bluex";
   zdialog     *zd = EFrgbrevise.zd;

   erase_toptext(101);                                                           //  erase prior labels from image

   for (int ii = 0; ii < 9; ii++)                                                //  loop slots 0-8
   {
      pixx[3] = '1' + ii;                                                        //  widget names "pix1" ... "pix9"
      redx[3] = '1' + ii;                                                        //  widget names "red1" ... "red9"
      greenx[5] = '1' + ii;
      bluex[4] = '1' + ii;

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

      snprintf(text,100,"%s %4d %4d",lab1[ii],px,py);                            //  format pixel "A nnnn nnnn"
      zdialog_stuff(zd,pixx,text);                                               //  pixel >> widget

      add_toptext(101,px,py,lab2[ii],"Sans 8");                                  //  paint label on image at pixel

      red1 = ramp_color_val1[ii][0];                                             //  original RGB values for pixel
      green1 = ramp_color_val1[ii][1];
      blue1 = ramp_color_val1[ii][2];
      red3 = ramp_color_val3[ii][0];                                             //  revised RGB values
      green3 = ramp_color_val3[ii][1];
      blue3 = ramp_color_val3[ii][2];

      if (ramp_color_metric == 2) {                                              //  convert to EV units if needed
         red1 = RGB2EV(red1);
         green1 = RGB2EV(green1);
         blue1 = RGB2EV(blue1);
         red3 = RGB2EV(red3);
         green3 = RGB2EV(green3);
         blue3 = RGB2EV(blue3);
      }

      if (ramp_color_metric == 3) {                                              //  or OD units
         red1 = RGB2OD(red1);
         green1 = RGB2OD(green1);
         blue1 = RGB2OD(blue1);
         red3 = RGB2OD(red3);
         green3 = RGB2OD(green3);
         blue3 = RGB2OD(blue3);
      }

      if (ramp_color_delta) {                                                    //  dialog is delta mode
         red3 -= red1;
         green3 -= green1;
         blue3 -= blue1;
      }

      zdialog_stuff(zd,redx,red3);
      zdialog_stuff(zd,greenx,green3);
      zdialog_stuff(zd,bluex,blue3);
   }

   zdialog_stuff(zd,"blend",ramp_color_blend);

   signal_thread();
   return;
}


//  thread function - multiple working threads to update image

void * ramp_color_thread(void *)
{
   void  * ramp_color_wthread(void *arg);                                        //  worker thread

   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request
      zsleep(0.3);                                                               //  more time for dialog controls

      if (ramp_color_npix < 1) continue;                                         //  must have 1+ pixels in table

      for (int ii = 0; ii < NWT; ii++)                                           //  start worker threads
         start_wthread(ramp_color_wthread,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

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

   return 0;                                                                     //  not executed, stop warning
}


void * ramp_color_wthread(void *arg)                                             //  worker thread function
{
   int         index = *((int *) (arg));
   int         px1, py1, px2, py2, ii;
   float       *pix1, *pix3;
   float       dist[9], weight[9], sumdist;
   float       blend, delta, red, green, blue, max;

   blend = E1pxm->ww;
   if (E1pxm->hh > E1pxm->ww) blend = E1pxm->hh;
   blend = blend * blend;
   blend = 0.0002 * ramp_color_blend * ramp_color_blend * blend + 100;           //  100 to 2 * E1pxm->ww**2

   for (ii = 0; ii < 9; ii++) {                                                  //  stop misleading gcc warnings
      dist[ii] = 1;
      weight[ii] = 1;                                                            //  avoid gcc warning                  16.04
   }

   for (py1 = index; py1 < E3pxm->hh; py1 += NWT)                                //  loop all image pixels
   for (px1 = 0; px1 < E3pxm->ww; px1++)
   {
      for (sumdist = ii = 0; ii < ramp_color_npix; ii++)                         //  compute weight of each revision point
      {
         px2 = ramp_color_pixel[ii][0];
         py2 = ramp_color_pixel[ii][1];
         dist[ii] = (px1-px2)*(px1-px2) + (py1-py2)*(py1-py2);                   //  distance (px1,py1) to (px2,py2)
         dist[ii] = 1.0 / (dist[ii] + blend);                                    //  blend reduces peaks at revision points
         sumdist += dist[ii];                                                    //  sum inverse distances
      }

      for (ii = 0; ii < ramp_color_npix; ii++)                                   //  weight of each point
         weight[ii] = dist[ii] / sumdist;

      pix1 = PXMpix(E1pxm,px1,py1);                                              //  input pixel
      pix3 = PXMpix(E3pxm,px1,py1);                                              //  output pixel

      red = pix1[0];
      green = pix1[1];
      blue = pix1[2];

      for (ii = 0; ii < ramp_color_npix; ii++) {                                 //  apply weighted color changes
         delta = ramp_color_val3[ii][0] - ramp_color_val1[ii][0];                //    to each color
         red += weight[ii] * delta;
         delta = ramp_color_val3[ii][1] - ramp_color_val1[ii][1];
         green += weight[ii] * delta;
         delta = ramp_color_val3[ii][2] - ramp_color_val1[ii][2];
         blue += weight[ii] * delta;
      }

      max = red;
      if (green > max) max = green;
      if (blue > max) max = blue;

      if (max > 255.9) {                                                         //  stop overflow/underflow
         red = red * 255.9 / max;
         green = green * 255.9 / max;
         blue = blue * 255.9 / max;
      }

      if (red < 0) red = 0;
      if (green < 0) green = 0;
      if (blue < 0) blue = 0;

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

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


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

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

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

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

editfunc    EFmatchcolor;


//  menu function

void m_match_color(GtkWidget *, const char *)
{
   int    match_color_dialog_event(zdialog* zd, const char *event);

   cchar    *title = ZTX("Color Match Images");

   F1_help_topic = "match_colors";
   if (checkpend("all")) return;                                                 //  check, no block: edit_setup() follows

/*
               Color Match Images
         1  [ 10 ]   mouse radius for color sample
         2  [Open]   image for source color
         3  click on image to get source color
         4  [Open]   image for target color
         5  click on image to set target color
                             [done] [cancel]
*/

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

   zdialog_stuff(zd,"radius",match_color_radius);                                //  remember last radius

   EFmatchcolor.funcname = "match_color";
   EFmatchcolor.Farea = 1;                                                       //  select area ignored
   EFmatchcolor.zd = zd;
   EFmatchcolor.threadfunc = match_color_thread;
   EFmatchcolor.mousefunc = match_color_mousefunc;

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

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


//  match_color dialog event and completion function

int match_color_dialog_event(zdialog *zd, const char *event)                     //  match_color dialog event function
{
   int      err;

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   
   if (zd->zstat)
   {
      if (match_color_mode == 4) {                                               //  edit was started
         if (zd->zstat == 1) edit_done(0);                                       //  commit edit
         else edit_cancel(0);                                                    //  discard edit
         match_color_mode = 0;
         return 1;
      }

      freeMouse();                                                               //  abandoned
      zdialog_free(zd);
      match_color_mode = 0;
      return 1;
   }

   if (strmatch(event,"radius"))                                                 //  set new mouse radius
      zdialog_fetch(zd,"radius",match_color_radius);

   if (strmatch(event,"open1"))                                                  //  get image 1 for color source
   {
      if (match_color_mode == 4) edit_cancel(1);                                 //  cancel edit, keep dialog           15.09
      match_color_mode = 0;
      err = f_open(null);                                                        //  open image 1
      if (! err) match_color_mode = 1;                                           //  image 1 ready to click
   }

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

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


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

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

   int      px, py;

   if (match_color_mode < 1) return;                                             //  no image available yet

   draw_mousecircle(Mxposn,Myposn,match_color_radius,0);                         //  draw circle around pointer

   if (LMclick)
   {
      LMclick = 0;
      px = Mxclick;
      py = Myclick;

      if (match_color_mode == 1 || match_color_mode == 2)                        //  image 1 ready to click
      {
         match_color_getRGB(px,py,match_color_RGB1);                             //  get RGB1 color
         match_color_mode = 2;
         return;
      }

      if (match_color_mode == 3 || match_color_mode == 4)                        //  image 2 ready to click
      {
         if (match_color_mode == 4) edit_reset();
         else {
            if (! edit_setup(EFmatchcolor)) return;                              //  setup edit - thread will launch
            match_color_mode = 4;                                                //  edit waiting for cancel or done
         }

         match_color_getRGB(px,py,match_color_RGB2);                             //  get RGB2 color
         signal_thread();                                                        //  update the target image
         return;
      }
   }

   return;
}


//  get the RGB averages for pixels within mouse radius

void match_color_getRGB(int px, int py, float rgb[3])
{
   int      radflat1 = match_color_radius;
   int      radflat2 = radflat1 * radflat1;
   int      rad, npix, qx, qy;
   float    red, green, blue;
   float    *pix1;
   PXM      *pxm;

   pxm = PXM_load(curr_file,1);                                                  //  popup ACK if error
   if (! pxm) return;

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

   for (qy = py-radflat1; qy <= py+radflat1; qy++)
   for (qx = px-radflat1; qx <= px+radflat1; qx++)
   {
      if (qx < 0 || qx > pxm->ww-1) continue;
      if (qy < 0 || qy > pxm->hh-1) continue;
      rad = (qx-px) * (qx-px) + (qy-py) * (qy-py);
      if (rad > radflat2) continue;
      pix1 = PXMpix(pxm,qx,qy);
      red += pix1[0];
      green += pix1[1];
      blue += pix1[2];
      npix++;
   }

   rgb[0] = red / npix;
   rgb[1] = green / npix;
   rgb[2] = blue / npix;

   PXM_free(pxm);
   return;
}


//  thread function - start multiple working threads

void * match_color_thread(void *)
{
   void  * match_color_wthread(void *arg);                                       //  worker thread

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

      for (int ii = 0; ii < NWT; ii++)                                           //  start worker threads
         start_wthread(match_color_wthread,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

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

   return 0;                                                                     //  not executed, avoid warning
}


void * match_color_wthread(void *arg)                                            //  worker thread function
{
   int         index = *((int *) (arg));
   int         px, py;
   float       *pix3;
   float       Rred, Rgreen, Rblue;
   float       red, green, blue, cmax;

   Rred = match_color_RGB1[0] / match_color_RGB2[0];                             //  color adjustment ratios
   Rgreen = match_color_RGB1[1] / match_color_RGB2[1];
   Rblue = match_color_RGB1[2] / match_color_RGB2[2];

   for (py = index; py < E3pxm->hh; py += NWT)                                   //  loop all image pixels
   for (px = 0; px < E3pxm->ww; px++)
   {
      pix3 = PXMpix(E3pxm,px,py);

      red = pix3[0] * Rred;                                                      //  adjust colors
      green = pix3[1] * Rgreen;
      blue = pix3[2] * Rblue;

      cmax = red;                                                                //  check for overflow
      if (green > cmax) cmax = green;
      if (blue > cmax) cmax = blue;

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

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

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


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

//  convert color profile of current image

editfunc    EFcolorprof;
char        colorprof1[200] = "/usr/share/color/icc/colord/sRGB.icc";
char        colorprof2[200] = "/usr/share/color/icc/colord/AdobeRGB1998.icc";


//  menu function

void m_color_profile(GtkWidget *, cchar *menu)
{
   int colorprof_dialog_event(zdialog *zd, cchar *event);

   zdialog     *zd;

   F1_help_topic = "color_profile";

   EFcolorprof.menufunc = m_color_profile;
   EFcolorprof.funcname = "color_profile";
   EFcolorprof.Frestart = 1;                                                     //  allow restart
   if (! edit_setup(EFcolorprof)) return;                                        //  setup edit

   zd = zdialog_new(ZTX("Change Color Profile"),Mwin,Bapply,Bdone,Bcancel,null);
   EFcolorprof.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",ZTX("input profile"),"space=5");
   zdialog_add_widget(zd,"entry","prof1","hb1",0,"expand|size=50");
   zdialog_add_widget(zd,"button","butt1","hb1",Bbrowse,"space=5");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","lab2","hb2",ZTX("output profile"),"space=5");
   zdialog_add_widget(zd,"entry","prof2","hb2",0,"expand|size=50");
   zdialog_add_widget(zd,"button","butt2","hb2",Bbrowse,"space=5");

   zdialog_stuff(zd,"prof1",colorprof1);
   zdialog_stuff(zd,"prof2",colorprof2);

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


//  dialog event and completion callback function

int colorprof_dialog_event(zdialog *zd, cchar *event)
{
   cchar    *title = ZTX("color profile");
   char     *file;
   float    *fpix1, *fpix2;
   float    f256 = 1.0 / 256.0;
   uint     Npix, nn;

   cmsHTRANSFORM  cmsxform;
   cmsHPROFILE    cmsprof1, cmsprof2;

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 2;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel                    15.07
   
   if (strmatch(event,"butt1")) {
      zdialog_fetch(zd,"prof1",colorprof1,200);                                  //  select input profile
      file = zgetfile(title,MWIN,"file",colorprof1);
      if (! file) return 1;
      zdialog_stuff(zd,"prof1",file);
      zfree(file);
   }

   if (strmatch(event,"butt2")) {
      zdialog_fetch(zd,"prof2",colorprof2,200);                                  //  select output profile
      file = zgetfile(title,MWIN,"file",colorprof2);
      if (! file) return 1;
      zdialog_stuff(zd,"prof2",file);
      zfree(file);
   }

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

   if (zd->zstat == 1) zd->zstat = 0;                                            //  apply, keep dialog open

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

   zdialog_fetch(zd,"prof1",colorprof1,200);                                     //  apply, get final profiles
   zdialog_fetch(zd,"prof2",colorprof2,200);

   cmsprof1 = cmsOpenProfileFromFile(colorprof1,"r");
   if (! cmsprof1) {
      zmessageACK(Mwin,ZTX("unknown cms profile %s"),colorprof1);
      return 1;
   }

   cmsprof2 = cmsOpenProfileFromFile(colorprof2,"r");
   if (! cmsprof2) {
      zmessageACK(Mwin,ZTX("unknown cms profile %s"),colorprof2);
      return 1;
   }

   //  calculate the color space transformation table

   cmsxform = cmsCreateTransform(cmsprof1,TYPE_RGB_FLT,cmsprof2,TYPE_RGB_FLT,INTENT_PERCEPTUAL,0);
   if (! cmsxform) {
      zmessageACK(Mwin,"cmsCreateTransform() failed");
      return 1;
   }

   fpix1 = E0pxm->pixels;                                                        //  input and output pixels
   fpix2 = E3pxm->pixels;
   Npix = E0pxm->ww * E0pxm->hh;

   Ffuncbusy = 1;

   for (uint ii = 0; ii < 3 * Npix; ii++)                                        //  rescale to range 0 - 0.9999
      fpix2[ii] = f256 * fpix1[ii];

   while (Npix)
   {
      nn = Npix;
      if (nn > 100000) nn = 100000;
      cmsDoTransform(cmsxform,fpix2,fpix2,nn);                                   //  speed: 3 megapixels/sec for 3 GHz CPU
      fpix2 += nn * 3;
      Npix -= nn;
      zmainloop(20);                                                             //  keep GTK alive
   }

   fpix2 = E3pxm->pixels;
   Npix = E0pxm->ww * E0pxm->hh;
   for (uint ii = 0; ii < 3 * Npix; ii++) {                                      //  rescale back to 0 - 255.99
      fpix2[ii] = fpix2[ii] * 256.0;
      if (fpix2[ii] >= 256.0) fpix2[ii] = 255.99;
   }

   Ffuncbusy = 0;

   cmsDeleteTransform(cmsxform);                                                 //  free resources
   cmsCloseProfile(cmsprof1);
   cmsCloseProfile(cmsprof2);

   CEF->Fmods++;                                                                 //  image is modified
   CEF->Fsaved = 0;
   Fpaint2();                                                                    //  update window image

   return 1;
}


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

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

namespace dust_names
{
   editfunc    EFdust;

   int         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 *, const char *)
{
   using namespace dust_names;

   int    dust_dialog_event(zdialog *zd, cchar *event);
   void * dust_thread(void *);

   F1_help_topic = "remove_dust";

   EFdust.menufunc = m_remove_dust;
   EFdust.funcname = "remove_dust";
   EFdust.Farea = 2;                                                             //  select area usable
   EFdust.Frestart = 1;                                                          //  restart allowed
   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);                                               //  maps pixels to assigned groups

   cc = E1pxm->ww * E1pxm->hh * sizeof(spixstack);
   pixstack = (spixstack *) zmalloc(cc);                                         //  pixel group search stack

/***
                     Remove Dust

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

                                  [Done] [Cancel]
***/

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

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

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

   signal_thread();
   return;
}


//  dialog event and completion callback function

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

   void dust_erase();

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   
   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  done
         wrapup_thread(8);                                                       //  thread finish, exit
         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 (strstr("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

      signal_thread();                                                           //  do the work
   }

   if (strmatch(event,"erase")) dust_erase();
   if (strmatch(event,"blendwidth")) 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;

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

      paintlock(1);                                                              //  block window updates               16.02
      PXM_free(E3pxm);
      E3pxm = PXM_copy(E9pxm);
      paintlock(0);                                                              //  unblock window updates             16.02

      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 == 3 && ! 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 bug");
                  break;

               case 3:
                  npx = px - dy;
                  npy = py - dx;
                  pixstack[kk].direc = 4;
                  break;

               case 4:
                  npx = px - dx;
                  npy = py + dy;
                  pixstack[kk].direc = 5;
                  break;

               case 5:
                  npx = px - dy;
                  npy = py + dx;
                  pixstack[kk].direc = 6;
                  break;

               case 6:
                  npx = px + dx;
                  npy = py - dy;
                  pixstack[kk].direc = 7;
                  break;

               case 7:
                  npx = px + dy;
                  npy = py - dx;
                  pixstack[kk].direc = 'x';
                  break;

               default:
                  npx = npy = 0;
                  zappcrash("stack search bug");
            }

            if (npx < 0 || npx > 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 == 3 && ! 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;                                                                     //  not executed, stop g++ warning
}


//  erase the selected dust areas

void dust_erase()
{
   using namespace dust_names;

   int         cc, ii, px, py, inc;
   int         qx, qy, npx, npy;
   int         sx, sy, tx, ty;
   int         rad, dist, dist2, mindist2;
   float       slope, f1, f2;
   float       *pix1, *pix3;
   char        *pmap;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);                         //  15.09

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

   cc = E1pxm->ww * E1pxm->hh;                                                   //  allocate pixel done map
   pmap = (char *) zmalloc(cc);
   memset(pmap,0,cc);

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

   if (sa_stat == 3)                                                             //  area edge blending
   {
      for (ii = 0; ii < E1pxm->ww * E1pxm->hh; ii++)                             //  find pixels in select area
      {
         dist = sa_pixmap[ii];
         if (! dist || dist >= sa_blend) continue;

         py = ii / E1pxm->ww;
         px = ii - py * E1pxm->ww;
         pix1 = PXMpix(E1pxm,px,py);                                             //  input pixel, unchanged image
         pix3 = PXMpix(E3pxm,px,py);                                             //  output pixel, changed image

///      f2 = 1.0 * dist / sa_blend;                                             //  changes over distance sa_blend
         f2 = sa_blendfunc(dist);                                                //  16.08
         f1 = 1.0 - f2;

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

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


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

//  anti-alias menu function

editfunc    EFantialias;                                                         //  edit function data


//  menu function

void m_anti_alias(GtkWidget *, const char *)
{
   int    antialias_dialog_event(zdialog* zd, const char *event);

   zdialog     *zd;

   F1_help_topic = "anti_alias";

   EFantialias.menufunc = m_anti_alias;
   EFantialias.funcname = "anti_alias";                                          //  function name
   EFantialias.Farea = 2;                                                        //  select area usable
   if (! edit_setup(EFantialias)) return;                                        //  setup edit

   zd = zdialog_new("Anti-Alias",Mwin,Bapply,Bcancel,null);                      //  setup dialog
   EFantialias.zd = zd;

   zdialog_resize(zd,200,0);
   zdialog_run(zd,antialias_dialog_event,"save");                                //  run dialog - parallel

   return;
}


//  antialias dialog event and completion function

int antialias_dialog_event(zdialog *zd, const char *event)                       //  antialias dialog event function
{
   void  antialias_func();

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   
   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  done
         antialias_func();                                                       //  apply
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   return 1;
}


//  compare two pixels
//  1.0 = perfect match, 0.0 = perfect mismatch (black/white)

inline float antialias_match(float *pix1, float *pix2)
{
   return PIXMATCH(pix1,pix2);                                                   //  0..1 = zero..perfect match
}


//  antialias function

/***
      Algorithm is like scale2x but adapted for photos
      instead of computer generated pixel art.
       _________________
      |     |     |     |
      |  A  |  B  |  C  |
      |_____|_____|_____|         _____________
      |     |     |     |        |      |      |
      |  D  |  E  |  F  |        |  E0  |  E1  |
      |_____|_____|_____|        |______|______|
      |     |     |     |        |      |      |
      |  G  |  H  |  I  |        |  E2  |  E3  |
      |_____|_____|_____|        |______|______|


      1. make output image = 2x input image dimensions
      2. loop for each pixel E in input image:
      3.    for corresponding pixels E0 E1 E2 E3 in output image:
      4.       if D:B match more than E:D and E:B
                  E0 = 0.333 * (D + B + E)
               else E0 = E
      5.       same for E1, E2, E3
      6. add some annealing and blur

***/

void antialias_func()
{
   int      ww, hh, ww2, hh2;
   int      px, py, px2, py2, qx, qy;
   int      ii, dist, rgb;
   float    *pixB, *pixD, *pixE, *pixF, *pixH;
   float    *pixE0, *pixE1, *pixE2, *pixE3;
   float    matchDB, matchBF, matchDH, matchHF;
   float    matchEB, matchED, matchEF, matchEH;
   float    red, green, blue, *pixel;
   float    blurf1, blurf2;

   paintlock(1);                                                                 //  block window updates               16.02

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

   ww2 = 2 * ww;                                                                 //  create 2x output image
   hh2 = 2 * hh;
   E9pxm = PXM_rescale(E1pxm,ww2,hh2);

//  calculate each output pixel group E0 E1 E2 E3
//  from input pixel E and its 8 neighbors

   for (py = 1; py < hh-1; py++)                                                 //  loop all (inside) input pixels
   for (px = 1; px < ww-1; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E1pxm->ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      pixB = PXMpix(E1pxm,px  ,py-1);
      pixD = PXMpix(E1pxm,px-1,py);
      pixE = PXMpix(E1pxm,px,  py);
      pixF = PXMpix(E1pxm,px+1,py);
      pixH = PXMpix(E1pxm,px  ,py+1);

      px2 = px * 2;                                                              //  2x2 pixel block in output image
      py2 = py * 2;                                                              //    corresponding to (px,py)

      pixE0 = PXMpix(E9pxm,px2,py2);
      pixE1 = PXMpix(E9pxm,px2+1,py2);
      pixE2 = PXMpix(E9pxm,px2,py2+1);
      pixE3 = PXMpix(E9pxm,px2+1,py2+1);

      matchDB = antialias_match(pixD,pixB);
      matchBF = antialias_match(pixB,pixF);
      matchDH = antialias_match(pixD,pixH);
      matchHF = antialias_match(pixH,pixF);
      matchEB = antialias_match(pixE,pixB);
      matchED = antialias_match(pixE,pixD);
      matchEF = antialias_match(pixE,pixF);
      matchEH = antialias_match(pixE,pixH);

      for (rgb = 0; rgb < 3; rgb++)                                              //  fill 2x2 pixel block
      {
         if (matchDB > matchED && matchDB > matchEB)
            pixE0[rgb] = 0.25 * (pixD[rgb] + pixB[rgb]) + 0.5 * pixE[rgb];
         else pixE0[rgb] = pixE[rgb];

         if (matchBF > matchEB && matchBF > matchEF)
            pixE1[rgb] = 0.25 * (pixB[rgb] + pixF[rgb]) + 0.5 * pixE[rgb];
         else pixE1[rgb] = pixE[rgb];

         if (matchDH > matchED && matchDH > matchEH)
            pixE2[rgb] = 0.25 * (pixD[rgb] + pixH[rgb]) + 0.5 * pixE[rgb];
         else pixE2[rgb] = pixE[rgb];

         if (matchHF > matchEH && matchHF > matchEF)
            pixE3[rgb] = 0.25 * (pixH[rgb] + pixF[rgb]) + 0.5 * pixE[rgb];
         else pixE3[rgb] = pixE[rgb];
      }
   }

//  apply a little bit of blur to each output pixel

   blurf1 = 0.6;                                                                 //  contribution of original pixel
   blurf2 = (1.0 - blurf1) / 9.0;                                                //  contribution of surrounding pixels

   for (py = 1; py < hh2-1; py++)
   for (px = 1; px < ww2-1; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py/2 * E1pxm->ww + px/2;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      red = green = blue = 0;

      for (qy = py-1; qy <= py+1; qy++)
      for (qx = px-1; qx <= px+1; qx++)
      {
         pixel = PXMpix(E9pxm,qx,qy);
         red += pixel[0];
         green += pixel[1];
         blue += pixel[2];
      }

      pixel = PXMpix(E9pxm,px,py);
      pixel[0] = blurf1 * pixel[0] + blurf2 * red;
      pixel[1] = blurf1 * pixel[1] + blurf2 * green;
      pixel[2] = blurf1 * pixel[2] + blurf2 * blue;
   }

   PXM_free(E3pxm);                                                              //  E3 = E9 resized 1/2                16.03
   E3pxm = PXM_rescale(E9pxm,ww,hh);
   E9pxm = 0;
   paintlock(0);                                                                 //  unblock window updates             16.02

   if (sa_stat) sa_unselect();                                                   //  area is no longer valid

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


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

//  Correct chromatic abberation (1st order only) by stretching
//  or shrinking a selected RGB color plane.

editfunc    EFchromatic;
float       chromaticRed, chromaticBlue;
int         chromaticFarea;


//  menu function

void m_color_fringes(GtkWidget *, cchar *)
{
   int    chromatic_dialog_event(zdialog *zd, cchar *event);
   void * chromatic_thread(void *);

   cchar  *chromatic_message = ZTX(" Adjust each RGB color to minimize \n"
                                   " color fringes at the image extremes. ");

   F1_help_topic = "color_fringes";

   EFchromatic.menufunc = m_color_fringes;
   EFchromatic.funcname = "color_fringes";
   EFchromatic.Farea = 2;                                                        //  select area usable
   EFchromatic.threadfunc = chromatic_thread;
   if (! edit_setup(EFchromatic)) return;                                        //  setup edit

/***
              Chromatic Abberation

         Adjust each RGB color to minimize
         color fringes at the image extremes.

         Red   =======[]===========  [-2.1]
         Blue  ============[]======  [+2.3]

                           [done] [cancel]
***/

   zdialog *zd = zdialog_new(ZTX("Color Fringes"),Mwin,Bdone,Bcancel,null);
   EFchromatic.zd = zd;

   zdialog_add_widget(zd,"label","lab1","dialog",chromatic_message,"space=3");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=5|homog");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"space=5|homog|expand");
   zdialog_add_widget(zd,"vbox","vb3","hb1",0,"space=5|homog");
   zdialog_add_widget(zd,"label","labred","vb1",Bred);
   zdialog_add_widget(zd,"label","labblue","vb1",Bblue);
   zdialog_add_widget(zd,"hscale","red","vb2","-5.0|5.0|0.1|0.0");
   zdialog_add_widget(zd,"hscale","blue","vb2","-5.0|5.0|0.1|0.10");
   zdialog_add_widget(zd,"label","redval","vb3"," 0.0");
   zdialog_add_widget(zd,"label","blueval","vb3"," 0.0");

   chromaticRed = chromaticBlue = 0;
   chromaticFarea = 0;

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


//  dialog event and completion callback function

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

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   
   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  done
         if (chromaticFarea && sa_stat != 3)                                     //  extend to whole image if
            signal_thread();                                                     //    former select area now deleted
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   if (strmatch(event,"red")) {
      zdialog_fetch(zd,"red",chromaticRed);
      snprintf(text,8,"%+.1f",chromaticRed);
      zdialog_stuff(zd,"redval",text);
      signal_thread();
      return 1;
   }

   if (strmatch(event,"blue")) {
      zdialog_fetch(zd,"blue",chromaticBlue);
      snprintf(text,8,"%+.1f",chromaticBlue);
      zdialog_stuff(zd,"blueval",text);
      signal_thread();
      return 1;
   }

   return 1;
}


//  chromatic image and accumulate chromatic memory

void * chromatic_thread(void *)
{
   void  * chromatic_wthread(void *);                                            //  worker thread process

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

      for (int ii = 0; ii < NWT; ii++)                                           //  start worker threads
         start_wthread(chromatic_wthread,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

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

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


void * chromatic_wthread(void *arg)                                              //  worker thread
{
   int         index = *((int *) (arg));
   int         px3, py3, ii;
   int         xlo, xhi, ylo, yhi;
   float       px1, py1;
   float       vpix1[4], *pix3;
   float       cx, cy, fx, fy;

   cx = E3pxm->ww / 2.0;
   cy = E3pxm->hh / 2.0;

   if (sa_stat == 3) {
      chromaticFarea = 1;                                                        //  remember if area active
      xlo = sa_minx + index;                                                     //  set area limits
      xhi = sa_maxx;
      ylo = sa_miny + index;
      yhi = sa_maxy;
   }

   else {
      chromaticFarea = 0;
      xlo = index;                                                               //  set whole image limits
      xhi = E3pxm->ww;
      ylo = index;
      yhi = E3pxm->hh;
   }

   for (py3 = ylo; py3 < yhi; py3 += NWT)                                        //  loop all output pixels
   for (px3 = xlo; px3 < xhi; px3++)
   {
      if (chromaticFarea) {                                                      //  area active
         ii = py3 * E3pxm->ww + px3;
         if (! sa_pixmap[ii]) continue;                                          //  pixel not in area
      }

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

      fx = (px3 - cx) / cx;                                                      //  -1 to 0 to +1
      fy = (py3 - cy) / cy;

      px1 = px3 + fx * chromaticRed;                                             //  red shift
      py1 = py3 + fy * chromaticRed;
      vpixel(E1pxm,px1,py1,vpix1);
      pix3[0] = vpix1[0];

      px1 = px3 + fx * chromaticBlue;                                            //  blue shift
      py1 = py3 + fy * chromaticBlue;
      vpixel(E1pxm,px1,py1,vpix1);
      pix3[2] = vpix1[2];
   }

   exit_wthread();
   return 0;
}


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

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

namespace stuckpix_names
{
   editfunc    EFstuckpix;
   int         stuckpix_1x1, stuckpix_2x2, stuckpix_3x3;                         //  pixel blocks to search
   cchar       *stuckpix_mode;                                                   //  find or fix
   float       stuckpix_threshcon;                                               //  min. contrast threshold
   char        *stuckpix_file = 0;                                               //  file for saved stuck pixels

   struct stuckpix_t {                                                           //  memory for stuck pixels
      int      px, py;                                                           //  location (NW corner of block)
      int      size;                                                             //  size: 1/2/3 = 1x1/2x2/3x3 block
      float    pcon;                                                             //  contrast with surrounding pixels
      int      rgb[3];                                                           //  surrounding pixel mean RGB
   };
   stuckpix_t  stuckpix[50];
   int         maxstuck = 50, Nstuck;
}


//  menu function

void m_stuck_pixels(GtkWidget *, cchar *menu)
{
   using namespace stuckpix_names;

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

   zdialog     *zd;

   F1_help_topic = "stuck_pixels";

   EFstuckpix.menufunc = m_stuck_pixels;
   EFstuckpix.funcname = "stuck_pixels";                                         //  function name
   EFstuckpix.threadfunc = stuckpix_thread;                                      //  thread function
   if (! edit_setup(EFstuckpix)) return;                                         //  setup edit

/***
    ________________________________________________
   |         Fix Stuck Pixels                       |
   |                                                |
   |   pixel group   [x] 1x1  [x] 2x2  [x] 3x3      |
   |   contrast      =========[]==================  |
   |   stuck pixels  12                             |
   |                                                |
   |                 [open] [save] [done] [cancel]  |
   |________________________________________________|

***/

   zd = zdialog_new(ZTX("Stuck Pixels"),Mwin,Bopen,Bsave,Bdone,Bcancel,null);
   EFstuckpix.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=5|homog");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"space=5|expand|homog");
   zdialog_add_widget(zd,"label","labgroup","vb1",ZTX("pixel group"));
   zdialog_add_widget(zd,"label","labcont","vb1",Bcontrast);
   zdialog_add_widget(zd,"hbox","hbgroup","vb2");
   zdialog_add_widget(zd,"check","1x1","hbgroup","1x1","space=3");
   zdialog_add_widget(zd,"check","2x2","hbgroup","2x2","space=3");
   zdialog_add_widget(zd,"check","3x3","hbgroup","3x3","space=3");
   zdialog_add_widget(zd,"hscale","contrast","vb2","0.1|1.0|0.001|0.5","expand");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labstuck","hb2",ZTX("stuck pixels:"),"space=5");

   zdialog_stuff(zd,"1x1",1);                                                    //  initz. dialog controls
   zdialog_stuff(zd,"2x2",1);
   zdialog_stuff(zd,"3x3",1);

   stuckpix_1x1 = stuckpix_2x2 = stuckpix_3x3 = 1;                               //  corresp. data values
   stuckpix_threshcon = 0.5;

   if (! stuckpix_file) {                                                        //  default file
      stuckpix_file = (char *) zmalloc(200);                                     //  /.../.fotoxx/stuck-pixels
      snprintf(stuckpix_file,200,"%s/stuck-pixels",get_zuserdir());
   }

   zdialog_run(zd,stuckpix_dialog_event,"save");                                 //  run dialog
   zd_thread = zd;                                                               //  setup for thread event

   stuckpix_mode = "find";
   signal_thread();                                                              //  find and show the stuck pixels
   return;
}


//  dialog event function

int stuckpix_dialog_event(zdialog *zd, cchar *event)
{
   using namespace stuckpix_names;

   void  stuckpix_open();
   void  stuckpix_save();
   void  stuckpix_show();

   char     stuck_pixels[50];

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

   if (strmatch(event,"done")) zd->zstat = 3;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 3;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 4;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 4;                                  //  escape = cancel                    15.07
   
   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  open file
         zd->zstat = 0;                                                          //  keep dialog active
         stuckpix_open();
      }
      else if (zd->zstat == 2) {                                                 //  save file
         zd->zstat = 0;                                                          //  keep dialog active
         stuckpix_save();
      }
      else if (zd->zstat == 3) {                                                 //  done
         zd_thread = 0;
         stuckpix_mode = "apply";
         signal_thread();                                                        //  fix the stuck pixels
         edit_done(0);                                                           //  commit edit
         erase_topcircles();
      }
      else {                                                                     //  cancel
         zd_thread = 0;
         edit_cancel(0);                                                         //  discard edit
         erase_topcircles();
      }
      return 1;
   }

   if (strmatch(event,"stuck pixels")) {                                         //  update count in dialog
      snprintf(stuck_pixels,49,"%s %d",ZTX("stuck pixels:"),Nstuck);
      if (Nstuck >= maxstuck) strcat(stuck_pixels,"+");
      zdialog_stuff(zd,"labstuck",stuck_pixels);
   }

   if (strstr("1x1 2x2 3x3 contrast",event)) {
      zdialog_fetch(zd,"1x1",stuckpix_1x1);                                      //  get dialog inputs
      zdialog_fetch(zd,"2x2",stuckpix_2x2);
      zdialog_fetch(zd,"3x3",stuckpix_3x3);
      zdialog_fetch(zd,"contrast",stuckpix_threshcon);
      stuckpix_mode = "find";
      signal_thread();                                                           //  find and show stuck pixels
   }

   return 1;
}


//  load stuck pixel list from a previously saved file

void stuckpix_open()
{
   using namespace stuckpix_names;

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

   zdialog     *zd;

   zd = zdialog_new(ZTX("Load Stuck Pixels"),Mwin,Bopen,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",ZTX("File:"),"space=3");
   zdialog_add_widget(zd,"entry","file","hb1",0,"expand|size=40");
   zdialog_add_widget(zd,"button","browse","hb1",Bbrowse,"space=5");

   zdialog_stuff(zd,"file",stuckpix_file);

   zdialog_run(zd,stuckpix_open_dialog_event);
   zdialog_wait(zd);
   zdialog_free(zd);

   return;
}


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

   char     *pp, file[200];
   FILE     *fid = 0;
   int      zstat, nn, ii, px, py, size;

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   
   if (strmatch(event,"browse")) {
      pp = zgetfile(ZTX("Stuck Pixels file"),MWIN,"file",stuckpix_file);
      if (! pp) return 1;
      zdialog_stuff(zd,"file",pp);
      zfree(pp);
   }

   zstat = zd->zstat;                                                            //  completion button

   if (zstat == 1)                                                               //  open
   {
      zdialog_fetch(zd,"file",file,200);                                         //  get file from dialog
      if (stuckpix_file) zfree(stuckpix_file);
      stuckpix_file = zstrdup(file);

      fid = fopen(stuckpix_file,"r");                                            //  open file
      if (! fid) {
         zmessageACK(Mwin,ZTX("file not found"));
         return 1;
      }

      nn = fscanf(fid,"stuck pixels px py size");                                //  read headers

      for (ii = 0; ii < maxstuck; ii++)
      {
         nn = fscanf(fid," %5d %5d %5d ",&px,&py,&size);                         //  read stuck pixels data
         if (nn == EOF) break;
         if (nn != 3) break;
         stuckpix[ii].px = px;
         stuckpix[ii].py = py;
         stuckpix[ii].size = size;
         stuckpix[ii].pcon = 0;
      }

      Nstuck = ii;
      fclose(fid);

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

      stuckpix_mode = "file";                                                    //  process pixels
      signal_thread();
   }

   return 1;
}


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

void stuckpix_save()                                                             //  simplified
{
   using namespace stuckpix_names;

   char     *file;
   FILE     *fid = 0;
   int      ii, px, py, size;

   file = zgetfile(ZTX("Stuck Pixels file"),MWIN,"save",stuckpix_file);
   if (! file) return;

   if (stuckpix_file) zfree(stuckpix_file);
   stuckpix_file = file;

   fid = fopen(stuckpix_file,"w");                                               //  open file
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   fprintf(fid,"stuck pixels \n");                                               //  write headers
   fprintf(fid,"    px    py   size \n");

   for (ii = 0; ii < Nstuck; ii++)                                               //  write stuck pixel data
   {
      px = stuckpix[ii].px;
      py = stuckpix[ii].py;
      size = stuckpix[ii].size;
      fprintf(fid," %5d %5d %5d \n",px,py,size);
   }

   fclose(fid);
   return;
}


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

inline float  stuckpix_getcon(float rgbA[3], float rgbB[3])
{
   float    match;
   match = PIXMATCH(rgbA,rgbB);                                                  //  0..1 = zero..perfect match
   return (1.0 - match);
}


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

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

   void   stuckpix_pixelblock(int px, int py, int size, float rgb[3]);
   void   stuckpix_surroundings(int px, int py, int size, float rgb[3]);
   void   stuckpix_insert(int px, int py, int size, float pcon, float rgb[3]);
   void   stuckpix_show();

   int         px, py, qx, qy, ii, size;
   float       rgbA[3], rgbB[3];
   float       threshcon, pcon;
   float       *ppix;

   while (true)
   {
      thread_idle_loop();

      if (strmatch(stuckpix_mode,"find"))                                        //  find stuck pixels in image
      {
         threshcon = stuckpix_threshcon;                                         //  threshold contrast, 0.1 to 1.0
         Nstuck = 0;                                                             //  count stuck pixels found

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

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

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

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

      if (strmatch(stuckpix_mode,"file"))                                        //  process stuck pixels read from a file
      {
         for (ii = 0; ii < Nstuck; ii++)
         {
            px = stuckpix[ii].px;
            py = stuckpix[ii].py;
            size = stuckpix[ii].size;

            stuckpix_pixelblock(px,py,size,rgbA);
            stuckpix_surroundings(px,py,size,rgbB);
            pcon = stuckpix_getcon(rgbA,rgbB);

            stuckpix[ii].pcon = pcon;
            stuckpix[ii].rgb[0] = rgbB[0];
            stuckpix[ii].rgb[1] = rgbB[1];
            stuckpix[ii].rgb[2] = rgbB[2];
         }

         stuckpix_show();                                                        //  show the stuck pixels read
      }

      if (strmatch(stuckpix_mode,"apply"))                                       //  replace the stuck pixels
      {
         for (ii = 0; ii < Nstuck; ii++)                                         //  loop pixel groups found
         {
            px = stuckpix[ii].px;
            py = stuckpix[ii].py;
            size = stuckpix[ii].size;
            if (! size) continue;

            if (size == 1)                                                       //  1x1 pixel group
            {
               ppix = PXMpix(E3pxm,px,py);                                       //  replace pixel group
               ppix[0] = stuckpix[ii].rgb[0];
               ppix[1] = stuckpix[ii].rgb[1];
               ppix[2] = stuckpix[ii].rgb[2];
            }

            if (size == 2)                                                       //  2x2 pixel group
            {
               for (qy = py; qy < py+2; qy++)                                    //  replace pixel group
               for (qx = px; qx < px+2; qx++)
               {
                  ppix = PXMpix(E3pxm,qx,qy);
                  ppix[0] = stuckpix[ii].rgb[0];
                  ppix[1] = stuckpix[ii].rgb[1];
                  ppix[2] = stuckpix[ii].rgb[2];
               }
            }

            if (size == 3)                                                       //  3x3 pixel group
            {
               for (qy = py; qy < py+3; qy++)                                    //  replace pixel group
               for (qx = px; qx < px+3; qx++)
               {
                  ppix = PXMpix(E3pxm,qx,qy);
                  ppix[0] = stuckpix[ii].rgb[0];
                  ppix[1] = stuckpix[ii].rgb[1];
                  ppix[2] = stuckpix[ii].rgb[2];
               }
            }
         }

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

   return 0;
}


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

void stuckpix_pixelblock(int px, int py, int size, float rgb[3])
{
   float    *pix1, *pix2, *pix3, *pix4, *pix5, *pix6, *pix7, *pix8, *pix9;

   if (size == 1)                                                                //  1x1 block, 1 pixel
   {
      pix1 = PXMpix(E3pxm,px,py);
      rgb[0] = pix1[0];
      rgb[1] = pix1[1];
      rgb[2] = pix1[2];
   }

   if (size == 2)                                                                //  2x2 block, 4 pixels
   {
      pix1 = PXMpix(E3pxm,px,py);                                                //  15.09
      pix2 = PXMpix(E3pxm,px+1,py);
      pix3 = PXMpix(E3pxm,px,py+1);
      pix4 = PXMpix(E3pxm,px+1,py+1);
      rgb[0] = (pix1[0] + pix2[0] + pix3[0] + pix4[0]) / 4;
      rgb[1] = (pix1[1] + pix2[1] + pix3[1] + pix4[1]) / 4;
      rgb[2] = (pix1[2] + pix2[2] + pix3[2] + pix4[2]) / 4;
   }

   if (size == 3)                                                                //  3x3 block, 9 pixels
   {
      pix1 = PXMpix(E3pxm,px,py);                                                //  15.09
      pix2 = PXMpix(E3pxm,px+1,py);
      pix3 = PXMpix(E3pxm,px+2,py);
      pix4 = PXMpix(E3pxm,px,py+1);
      pix5 = PXMpix(E3pxm,px+1,py+1);
      pix6 = PXMpix(E3pxm,px+2,py+1);
      pix7 = PXMpix(E3pxm,px,py+2);
      pix8 = PXMpix(E3pxm,px+1,py+2);
      pix9 = PXMpix(E3pxm,px+2,py+2);

      rgb[0] = (pix1[0] + pix2[0] + pix3[0] + pix4[0] 
             + pix5[0] + pix6[0] + pix7[0] + pix8[0] + pix9[0]) / 9;
      rgb[1] = (pix1[1] + pix2[1] + pix3[1] + pix4[1] 
             + pix5[1] + pix1[1] + pix7[1] + pix8[1] + pix9[1]) / 9;
      rgb[2] = (pix1[2] + pix2[2] + pix3[2] + pix4[2] 
             + pix5[2] + pix6[2] + pix7[2] + pix8[2] + pix9[2]) / 9;
   }

   return;
}


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

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

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

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

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

   red = green = blue = 0;

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

      red = red / 8;                                                             //  average surrounding pixels
      green = green / 8;
      blue = blue / 8;
   }

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

      red = red / 12;                                                            //  average surrounding pixels
      green = green / 12;
      blue = blue / 12;
   }

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

      red = red / 16;                                                            //  average surrounding pixels
      green = green / 16;
      blue = blue / 16;
   }

   rgb[0] = red;
   rgb[1] = green;
   rgb[2] = blue;

   return;
}


//  draw circles around the stuck pixels

void stuckpix_show()
{
   using namespace stuckpix_names;

   int      ii, px, py, size, rad;

   erase_topcircles();                                                           //  erase prior circles

   for (ii = 0; ii < Nstuck; ii++)                                               //  write circles around stuck pixels
   {
      px = stuckpix[ii].px;
      py = stuckpix[ii].py;
      size = stuckpix[ii].size;
      if (! size) continue;
      rad = 8 + 2 * size;
      px += size / 2;
      py += size / 2;
      add_topcircle(px,py,rad);
   }

   zd_thread = EFstuckpix.zd;                                                    //  send stuck pixel count to zdialog
   zd_thread_event = "stuck pixels";

   Fpaint2();                                                                    //  update window
   return;
}


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

void stuckpix_insert(int px, int py, int size, float pcon, float rgb[3])
{
   using namespace stuckpix_names;

   int      ii, px1, py1, px2, py2, px3, py3, px4, py4;

   px1 = px - 1;                                                                 //  "touch" periphery
   py1 = py - 1;
   px2 = px + size + 1;
   py2 = py + size + 1;

   for (ii = 0; ii < Nstuck; ii++)                                               //  loop stuckpix table
   {
      if (stuckpix[ii].size == 0) continue;                                      //  skip deleted entry

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

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

         if (pcon < 1.0 * stuckpix[ii].pcon) return;                             //  keep only if contrast is greater

         stuckpix[ii].size = 0;                                                  //  delete old entry
      }
   }

   for (ii = 0; ii < Nstuck; ii++)                                               //  loop stuckpix table
      if (stuckpix[ii].size == 0) break;                                         //  find first empty slot or last + 1
   if (ii == maxstuck) return;                                                   //  table full

   stuckpix[ii].px = px;                                                         //  replace overlapping entry
   stuckpix[ii].py = py;                                                         //    or add to end of table
   stuckpix[ii].size = size;
   stuckpix[ii].pcon = pcon;
   stuckpix[ii].rgb[0] = rgb[0];
   stuckpix[ii].rgb[1] = rgb[1];
   stuckpix[ii].rgb[2] = rgb[2];

   if (ii == Nstuck) Nstuck++;                                                   //  incr. count if added to end
   return;
}



