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

   Fotocx - edit photos and manage collections

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

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

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

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

   Fotocx image edit - Refine menu functions

   m_edit_hist             edit brightness histogram
   m_flat_hist             flatten brightness histogram
   m_localcon              increase local contrast
   m_amplifycon            amplify existing contrast to improve visibility of details
   m_gretinex              rescale pixel RGB brightness range - entire image
   m_lretinex              rescale pixel RGB brightness range - within local areas
   m_saturation            adjust saturation based on pixel brightness
   m_soft_focus            apply a soft focus effect to an image
   m_match_colors          adjust image colors to match those in a chosen image
   m_brite_ramp            adjust brightness gradually across the image
   m_vignette              highlight selected image region

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

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

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

//  edit brightness histogram

namespace edit_hist_names
{
   float    LC, HC;                                                              //  low, high cutoff levels
   float    LF, MF, HF;                                                          //  low, mid, high flatten parms
   float    LS, MS, HS;                                                          //  low, mid, high stretch parms
   float    LR;                                                                  //  low ramp-up rate
   float    BB[1000];                                                            //  adjusted Br for input Br 0-999

   int    dialog_event(zdialog* zd, ch  *event);
   void   compute_BB();
   void * thread(void *);
   void * wthread(void *);

   editfunc    EFedit_hist;
}


//  menu function

void m_edit_hist(GtkWidget *, ch  *menu)
{
   using namespace edit_hist_names;

   ch   *title = "Edit Brightness Histogram";

   F1_help_topic = "edit histogram";

   Plog(1,"m_edit_hist \n");

   EFedit_hist.menuname = "Edit Histogram";
   EFedit_hist.menufunc = m_edit_hist;
   EFedit_hist.FprevReq = 1;                                                     //  preview
   EFedit_hist.Farea = 2;                                                        //  select area usable
   EFedit_hist.Fpaintedits = 1;                                                  //  use with paint edits OK
   EFedit_hist.Fscript = 1;                                                      //  scripting supported 
   EFedit_hist.threadfunc = thread;

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

/***
          __________________________________________
         |        Edit Brightness Histogram         |
         |                                          |
         | Low Cutoff   ==========[]==============  |
         | High Cutoff  ==============[]==========  |
         | Low Flatten  =========[]===============  |
         | Mid Flatten  ============[]============  |
         | High Flatten ==============[]==========  |
         | Low Stretch  ================[]========  |
         | Mid Stretch  =============[]===========  |
         | High Stretch =========[]===============  |
         | Low Ramp-up  =======[]=================  |
         |                                          |
         |                         [Reset] [OK] [X] |
         |__________________________________________|

***/

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

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

   zdialog_add_widget(zd,"label","labLC","vb1","Low Cutoff  ");
   zdialog_add_widget(zd,"label","labHC","vb1","High Cutoff ");
   zdialog_add_widget(zd,"label","labLF","vb1","Low Flatten ");
   zdialog_add_widget(zd,"label","labMF","vb1","Mid Flatten ");
   zdialog_add_widget(zd,"label","labHF","vb1","High Flatten");
   zdialog_add_widget(zd,"label","labLS","vb1","Low Stretch ");
   zdialog_add_widget(zd,"label","labMS","vb1","Mid Stretch ");
   zdialog_add_widget(zd,"label","labHS","vb1","High Stretch");
   zdialog_add_widget(zd,"label","labLR","vb1","Low Ramp-up ");

   zdialog_add_widget(zd,"hscale2","LC","vb2","0|1.0|0.01|0","expand");
   zdialog_add_widget(zd,"hscale2","HC","vb2","0|1.0|0.01|0","expand");
   zdialog_add_widget(zd,"hscale2","LF","vb2","0|1.0|0.01|0","expand");
   zdialog_add_widget(zd,"hscale2","MF","vb2","0|1.0|0.01|0","expand");
   zdialog_add_widget(zd,"hscale2","HF","vb2","0|1.0|0.01|0","expand");
   zdialog_add_widget(zd,"hscale2","LS","vb2","0|1.0|0.01|0","expand");
   zdialog_add_widget(zd,"hscale2","MS","vb2","0|1.0|0.01|0","expand");
   zdialog_add_widget(zd,"hscale2","HS","vb2","0|1.0|0.01|0","expand");
   zdialog_add_widget(zd,"hscale2","LR","vb2","0|1|0.01|0");                     //  24.60
   
   zdialog_rescale(zd,"LC",0,0,1);
   zdialog_rescale(zd,"HC",0,0,1);
   zdialog_rescale(zd,"LF",0,0,1);
   zdialog_rescale(zd,"MF",0,0,1);
   zdialog_rescale(zd,"HF",0,0,1);
   zdialog_rescale(zd,"LS",0,0,1);
   zdialog_rescale(zd,"MS",0,0,1);
   zdialog_rescale(zd,"HS",0,0,1);
   zdialog_rescale(zd,"LR",0,0,1);

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

   LC = HC = LF = MF = HF = LS = MS = HS = LR = 0;

   compute_BB();                                                                 //  compute full flatten for all pixels
   
   return;
}


//  dialog event and completion function

int edit_hist_names::dialog_event(zdialog *zd, ch  *event)
{
   using namespace edit_hist_names;
   
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  apply and quit
   if (strmatch(event,"apply")) zd->zstat = 2;                                   //  from script                           24.30
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  cancel

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

   if (zstrstr("LC HC LF MF HF LS MS HS LR apply",event))
   {
      zdialog_fetch(zd,"LC",LC);
      zdialog_fetch(zd,"HC",HC);
      zdialog_fetch(zd,"LF",LF);
      zdialog_fetch(zd,"MF",MF);
      zdialog_fetch(zd,"HF",HF);
      zdialog_fetch(zd,"LS",LS);
      zdialog_fetch(zd,"MS",MS);
      zdialog_fetch(zd,"HS",HS);
      zdialog_fetch(zd,"LR",LR);
      compute_BB();
      thread_signal();
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  reset
         zd->zstat = 0;                                                          //  keep dialog active
         LC = HC = LF = MF = HF = LS = MS = HS = LR = 0;
         zdialog_stuff(zd,"LC",LC);
         zdialog_stuff(zd,"HC",HC);
         zdialog_stuff(zd,"LF",LF);
         zdialog_stuff(zd,"MF",MF);
         zdialog_stuff(zd,"HF",HF);
         zdialog_stuff(zd,"LS",LS);
         zdialog_stuff(zd,"MS",MS);
         zdialog_stuff(zd,"HS",HS);
         zdialog_stuff(zd,"LR",LR);
         compute_BB();
         edit_reset();
      }
      else if (zd->zstat == 2) {                                                 //  done
         edit_fullsize();                                                        //  get full size image
         compute_BB();
         thread_signal();
         m_RGB_hist(0,"kill");                                                   //  kill RGB histogram graph
         edit_addhist("LC:%.3f HC:%.3f LF:%.3f MF:%.3f HF:%.3f "
                      "LS:%.3f MS:%.3f HS:%.3f LR:%.3f",                         //  edit params > edit hist
                       LC,HC,LF,MF,HF,LS,MS,HS);
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  cancel - discard edit

      return 1;
   }

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


//  compute flattened brightness levels for preview size or full size image
//  FB[B] = flattened brightness level for brightness B, scaled 0-1000

void edit_hist_names::compute_BB()
{
   using namespace edit_hist_names;

   int      ii, npix, py, px, iB;
   float    *pix1;
   float    B, LC2, HC2;
   float    FB[1000];
   float    LF2, MF2, HF2, LS2, MS2, HS2;
   float    LFB, MFB, HFB, LSB, MSB, HSB;
   float    LFW, MFW, HFW, LSW, MSW, HSW, TWB;

   for (ii = 0; ii < 1000; ii++)                                                 //  clear brightness histogram data
      FB[ii] = 0;

   if (sa_stat == sa_stat_fini)                                                  //  process selected area
   {
      for (ii = npix = 0; ii < Eww * Ehh; ii++)
      {
         if (! sa_pixmap[ii]) continue;
         py = ii / Eww;
         px = ii - py * Eww;
         pix1 = PXMpix(E1pxm,px,py);
         B = pix1[0];
         if (pix1[1] > B) B = pix1[1];                                           //  redefine brightness                   24.60
         if (pix1[2] > B) B = pix1[2];
         B = B / 256.0;
         FB[int(B)]++;
         npix++;
      }

      for (ii = 1; ii < 1000; ii++)                                              //  cumulative brightness histogram
         FB[ii] += FB[ii-1];                                                     //   0 ... npix

      for (ii = 0; ii < 1000; ii++)
         FB[ii] = FB[ii] / npix * 999.0;                                         //  flattened brightness level
   }

   else                                                                          //  process whole image
   {
      for (py = 0; py < Ehh; py++)                                               //  compute brightness histogram
      for (px = 0; px < Eww; px++)
      {
         pix1 = PXMpix(E1pxm,px,py);
         B = pix1[0];
         if (pix1[1] > B) B = pix1[1];                                           //  redefine brightness                   24.60
         if (pix1[2] > B) B = pix1[2];
         B = B / 256.0;
         FB[int(B)]++;
      }

      for (ii = 1; ii < 1000; ii++)                                              //  cumulative brightness histogram
         FB[ii] += FB[ii-1];                                                     //   0 ... (Eww * Ehh)
   
      for (ii = 0; ii < 1000; ii++)
         FB[ii] = FB[ii] / (Eww * Ehh) * 999.0;                                  //  flattened brightness level
   }

   LC2 = 500 * LC;                                                               //  low cutoff, 0 ... 500
   HC2 = 1000 - 500 * HC;                                                        //  high cutoff, 1000 ... 500

   for (iB = 0; iB < 1000; iB++)                                                 //  loop brightness 0 - 1000   
   {
      B = iB;

      if (LC2 > 0 || HC2 < 1000) {                                               //  stretch to cutoff limits
         if (B < LC2) B = 0;
         else if (B > HC2) B = 999;
         else B = 1000.0 * (B - (1 - LR) * LC2) / (HC2 - LC2);                   //  ramp up faster from low cutoff
      }
      
      if (B < 500) LF2 = LF * (500 - B) / 500;                                   //  low flatten  LF ... 0
      else LF2 = 0;
      LF2 = LF2 * LF2;
      LFB = LF2 * FB[iB] + (1.0 - LF2) * B;
      
      LS2 = LS * (1000 - B) / 1000;                                              //  low stretch  LS ... 0
      LS2 = LS2 * LS2;
      LSB = B * (1 - LS2);
      
      MF2 = MF * (500 - fabsf(B - 500)) / 500;                                   //  mid flatten  0 ... MF ... 0
      MF2 = MF2 * MF2;;
      MFB = MF2 * FB[iB] + (1.0 - MF2) * B;

      MS2 = MS * (B - 500) / 500;                                                //  mid stretch  -MS ... 0 ... MS
      MSB = B * (1 + 0.5 * MS2);
      
      if (B > 500) HF2 = HF * (B - 500) / 500;                                   //  high flatten  0 ... HF
      else HF2 = 0;
      HF2 = HF2 * HF2;
      HFB = HF2 * FB[iB] + (1.0 - HF2) * B;

      HS2 = HS * B / 1000;                                                       //  high stretch  0 ... HS
      HS2 = HS2 * HS2;
      HSB = B * (1 + HS2);
      
      LFW = fabsf(B - LFB) / (B + 1);                                            //  weight of each component
      LSW = fabsf(B - LSB) / (B + 1);
      MFW = fabsf(B - MFB) / (B + 1);
      MSW = fabsf(B - MSB) / (B + 1);
      HFW = fabsf(B - HFB) / (B + 1);
      HSW = fabsf(B - HSB) / (B + 1);
      
      TWB = LFW + LSW + MFW + MSW + HFW + HSW;                                   //  add weighted components
      if (TWB == 0) BB[iB] = B;
      else BB[iB] = (LFW * LFB + LSW * LSB
                   + MFW * MFB + MSW * MSB 
                   + HFW * HFB + HSW * HSB) / TWB;
   }
   
   return;
}


//  adjust brightness histogram thread function

void * edit_hist_names::thread(void *)
{
   using namespace edit_hist_names;

   if (Fpaintedits && ! Mdrag) return 0;                                         //  if mouse paint, must be drag
   
   get_edit_pixels_init(NSMP,0);                                                 //  initz. pixel loop

   do_wthreads(wthread,NSMP);

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

   if (! Fpaintedits) Fpaint2();                                                 //  update window

   return 0;
}


//  worker thread for each CPU processor core

void * edit_hist_names::wthread(void *arg)
{
   using namespace edit_hist_names;

   int         index = *((int *) (arg));
   int         iB, px, py, Fend;
   float       B, *pix1, *pix3;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       blend;

   while (true)
   {
      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) break;
      if (blend == 0) continue;
   
      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      B = pix1[0];
      if (pix1[1] > B) B = pix1[1];                                              //  redefine brightness                   24.60
      if (pix1[2] > B) B = pix1[2];
      B = B / 256.0 * 1000;
      iB = int(B);
      if (B > 0) B = BB[iB] / B;
      
      R1 = pix1[0];
      G1 = pix1[1];
      B1 = pix1[2];

      R3 = pix3[0];
      G3 = pix3[1];
      B3 = pix3[2];
      
      R9 = B * R1;                                                               //  new output pixel
      G9 = B * G1;
      B9 = B * B1;
      
      RGBfix(R9,G9,B9);

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

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

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

   return 0;
}


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

//  Flatten brightness histogram.
//  The new brightness for each pixel is based on the brightness histogram
//    of a zone of pixels within a given radius of the pixel.

namespace flat_hist_names
{
   PXB      *Rpxb;                     //  working image (reduced) 
   int      Rscale;                    //  working image downscale - N
   int      Rww, Rhh;                  //  working image size - Eww/N, Ehh/N
   int      zrad;                      //  radius of pixel zone in working image
   int      radius;                    //  corresp. radius in full-size image
   int      Nz;                        //  zone count (Rww * Rhh)
   float    *Bhist;                    //  brightness histogram per zone, Bhist[Nz][256]
   int      flatten;                   //  flatten slider, 0-100
   int      moderate;                  //  moderate slider, 0-100
   
   editfunc    EFflathist;
}


//  menu function

void m_flat_hist(GtkWidget *, ch  *menu)                                         //  rewrite  24.60
{
   using namespace flat_hist_names;

   int   flathist_dialog_event(zdialog *zd, ch *event);
   void  flathist_calczones();

   ch    *title = "Flatten Histogram";
   
   F1_help_topic = "flatten histogram";
   
   Plog(1,"m_flat_hist \n");
   
   if (! curr_file) {
      zmessageACK(Mwin,"no current file");
      return;
   }
   
   EFflathist.menuname = "Flatten Histogram";
   EFflathist.menufunc = m_flat_hist;
   EFflathist.Farea = 2;                                                         //  select area usable
   EFflathist.Fpaintedits = 1;                                                   //  use with paint edits OK
   EFflathist.Fscript = 1;                                                       //  scripting supported

   if (! edit_setup(EFflathist)) return;                                         //  setup edit
   
   Rpxb = PXB_load(curr_file,1);                                                 //  get image PXB - RGB 8-bits
   if (! Rpxb) {
      edit_cancel(0);
      return;
   }
                                                                                 //  compute working image downscale           
   Rscale = sqrtf(0.000001 * Eww * Ehh + 9);                                     //  1/3/10/30/100 megapix --> 3/3/4/6/10
   if (Eww * Ehh < MEGA) Rscale = 1;
   Plog(1,"Rscale: %d \n",Rscale);

   Rww = Eww / Rscale;                                                           //  get working image, 1/Rscale size
   Rhh = Ehh / Rscale;
   
   PXB *temppxb = PXB_rescale(Rpxb,Rww+Rscale,Rhh+Rscale);
   if (! temppxb) {
      edit_cancel(0);
      return;
   }
   
   PXB_free(Rpxb);
   Rpxb = temppxb;                                                               //  working image
   
   Nz = (Rww+Rscale) * (Rhh+Rscale);                                             //  zone count = pixel count
   uint cc = 256 * Nz * sizeof(float);
   Bhist = (float *) zmalloc(cc,"flathist");                                     //  Bhist[Nz][256]
   if (! Bhist) {
      PXB_free(Rpxb);
      edit_cancel(0);
      return;
   }
   
/***
          ________________________________
         |       Flatten Histogram        |
         |                                |
         | Radius [ 100 ]  [Apply]        |
         |                                |
         | Flatten =========[]==========  |
         | Moderate ==========[]========  |
         |                                |
         |               [Reset] [OK] [X] |
         |________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,"Reset","OK"," X ",0);
   EFflathist.zd = zd;
   
   zdialog_add_widget(zd,"hbox","hbrad","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labrad","hbrad","Radius","space=3");
   zdialog_add_widget(zd,"zspin","radius","hbrad","20|999|1|50");
   zdialog_add_widget(zd,"button","apply","hbrad","Apply","space=10");
   zdialog_add_widget(zd,"hbox","hbflat","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labflat","hbflat","Flatten","space=3");
   zdialog_add_widget(zd,"hscale","flatten","hbflat","0|100|1|0","expand");
   zdialog_add_widget(zd,"hbox","hbmod","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labmod","hbmod","Moderate","space=3");
   zdialog_add_widget(zd,"hscale","moderate","hbmod","0|100|1|0","expand");
   
   zdialog_rescale(zd,"flatten",0,0,100);                                        //  expand scale at low end               24.60
   
   zrad = 50;                                                                    //  initial zone radius (working image)
   radius = zrad * Rscale;                                                       //  corresp. full size image radius
   zdialog_stuff(zd,"radius",radius);

   flatten = 0;                                                                  //  initial dialog values
   moderate = 0;
   zdialog_stuff(zd,"flatten",flatten);
   zdialog_stuff(zd,"moderate",moderate);

   zdialog_run(zd,flathist_dialog_event,"save");                                 //  start dialog
   
   flathist_calczones();                                                         //  calculate zone histograms

   return;
}


//  dialog event and completion function

int flathist_dialog_event(zdialog *zd, ch *event)
{
   using namespace flat_hist_names;

   void  flathist_calczones();
   void  *flathist_thread(void *);
   
   int   Fdoflat = 0;

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  apply and quit
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  cancel
   if (strmatch(event,"paint")) Fdoflat = 1;
   
   if (strmatch(event,"apply"))                                                  //  new zone radius value
   {
      zdialog_fetch(zd,"radius",radius);
      if (zrad == radius / Rscale) return 1;                                     //  no change
      zrad = radius / Rscale;
      Funcbusy(+1,1);
      flathist_calczones();                                                      //  calculate new zone histograms
      Funcbusy(-1);
      Fdoflat = 1;
   }
   
   if (strmatch(event,"flatten"))                                                //  new flatten value
   {
      zdialog_fetch(zd,"flatten",flatten);
      Fdoflat = 1;
   }

   if (strmatch(event,"moderate"))                                               //  new moderate value
   {
      zdialog_fetch(zd,"moderate",moderate);
      Fdoflat = 1;
   }

   if (Fdoflat)                                                                  //  flatten histogram
   {
      Fdoflat = 0;

      if (flatten == 0) {
         edit_reset();
         return 1;
      }
      
      get_edit_pixels_init(NSMP,0);                                              //  initz. pixel loop
      do_wthreads(flathist_thread,NSMP);                                         //  flatten histogram

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

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

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  [reset]
      {
         zd->zstat = 0;
         edit_reset();
         zdialog_stuff(zd,"flatten",0);
         flatten = 0;
         return 1;
      }
      
      if (zd->zstat == 2)                                                        //  [OK]
      {
         PXB_free(Rpxb);
         zfree(Bhist);
         edit_addhist("Rad:%d Flat:%d",radius,flatten);                          //  edit params > edit hist
         edit_done(0);
         return 1;
      }
      
      if (zd->zstat)                                                             //  [ X ] or [-]
      {
         PXB_free(Rpxb);
         zfree(Bhist);
         edit_cancel(0);
         return 1;
      }
   }
      
   return 1;
}


//  Calculate the brightness histogram for the zone around each pixel.
//  From this, calculate brightness adjustment factors to flatten the histogram.

void flathist_calczones()
{
   using namespace flat_hist_names;
   
   int      zone;
   int      px, py, qx, qy;                                                      //  zone center pixel
   int      xlo, xhi, ylo, yhi;                                                  //  zone limits
   int      ii, lev, npix;
   uint8    *pix;
   
   for (zone = 0; zone < Nz; zone++)                                             //  loop all zones
   {
      for (lev = 0; lev < 256; lev++)                                            //  loop 256 brightness levels
      {
         ii = 256 * zone + lev;                                                  //  Bhist[zone][lev] = 0
         Bhist[ii] = 0;
      }

      py = zone / Rww;                                                           //  zone central pixel
      px = zone - py * Rww;

      xlo = px - zrad;                                                           //  get zone limits
      if (xlo < 0) xlo = 0;
      xhi = px + zrad;
      if (xhi > Rww) xhi = Rww;
      ylo = py - zrad;
      if (ylo < 0) ylo = 0;
      yhi = py + zrad;
      if (yhi > Rhh) yhi = Rhh;

      if (px == 0)                                                               //  first pixel in row
      {
         for (qy = ylo; qy < yhi; qy++)                                          //  loop all pixels in zone
         for (qx = xlo; qx < xhi; qx++)
         {
            pix = PXBpix(Rpxb,qx,qy);
            lev = pix[0] + pix[1] + pix[2];
            lev = lev / 3;                                                       //  pixel brightness 0-255
            ii = 256 * zone + lev;
            Bhist[ii] += 1;                                                      //  pixel count per brightness level
         }
      }
      
      if (px > 0)                                                                //  2nd or later pixel in row
      {
         for (lev = 0; lev < 256; lev++)
         {
            ii = 256 * zone + lev;                                               //  copy previous histogram
            Bhist[ii] = Bhist[ii-256];
         }
         
         if (xlo > 0)                                                            //  subtract 1st col. of previous hist.
         {
            qx = xlo - 1;
            for (qy = ylo; qy < yhi; qy++)
            {
               pix = PXBpix(Rpxb,qx,qy);
               lev = pix[0] + pix[1] + pix[2];
               lev = lev / 3; 
               ii = 256 * zone + lev;
               Bhist[ii] -= 1;
            }
         }
         
         if (xhi < Rww)                                                          //  add last col. of this hist.
         {
            qx = xhi - 1;
            for (qy = ylo; qy < yhi; qy++)
            {
               pix = PXBpix(Rpxb,qx,qy);
               lev = pix[0] + pix[1] + pix[2];
               lev = lev / 3; 
               ii = 256 * zone + lev;
               Bhist[ii] += 1;
            }
         }
      }
   }

   for (zone = 0; zone < Nz; zone++)                                             //  loop all zones
   {
      py = zone / Rww;                                                           //  zone central pixel
      px = zone - py * Rww;

      xlo = px - zrad;                                                           //  get zone limits
      if (xlo < 0) xlo = 0;
      xhi = px + zrad;
      if (xhi > Rww) xhi = Rww;
      ylo = py - zrad;
      if (ylo < 0) ylo = 0;
      yhi = py + zrad;
      if (yhi > Rhh) yhi = Rhh;

      for (lev = 1; lev < 256; lev++)                                            //  loop all levels in zone
      {
         ii = 256 * zone + lev;
         Bhist[ii] += Bhist[ii-1];                                               //  cumulative pixel count < level
      }

      npix = (yhi - ylo + 1) * (xhi - xlo + 1);                                  //  pixels in zone
            
      for (lev = 0; lev < 256; lev++)
      {
         ii = 256 * zone + lev;
         Bhist[ii] = Bhist[ii] / npix * 256 / (lev + 1);                         //  brightness adjustment factor
      }
   }
   
   return;
}


//  thread function to set output pixel brightness from input pixel 
//  brightness and corresponding brightness adjustment factor 

void * flathist_thread(void *arg)
{
   using namespace flat_hist_names;
   
   int      index, zone, Fend;
   int      px, py, ii, B;
   float    blend;
   float    R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float    BF, BF1, BF2, MF1, MF2;
   float    *pix1, *pix3;

   index = *((int *) arg);

   BF1 = 0.01 * flatten;                                                         //  flatten factor 0 .. 1
   BF2 = 1.0 - BF1;

   MF1 = 0.01 * moderate;                                                        //  moderate factor 0 .. 1
   MF2 = 1.0 - MF1;
   
   while (true)
   {
      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) break;
      if (blend == 0) continue;
      
      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      
      R1 = pix1[0];                                                              //  input RGB
      G1 = pix1[1];
      B1 = pix1[2];
      
      R3 = pix3[0];
      G3 = pix3[1];
      B3 = pix3[2];

      B = 0.333 * (R1 + G1 + B1);                                                //  input pixel brightness 0-255
      zone = py / Rscale * Rww + px / Rscale;                                    //  input pixel zone
      ii = 256 * zone + B;
      BF = Bhist[ii];                                                            //  flatten brightness factor  0 .. N
      
      BF = sqrtf(BF) * MF1 + BF * MF2;                                           //  moderated by moderate slider
      
      BF = BF * BF1 + BF2;                                                       //  moderated by flatten slider
      
      R9 = R1 * BF;                                                              //  new brightness
      G9 = G1 * BF;
      B9 = B1 * BF;
      
      RGBfix(R9,G9,B9);

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

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

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

   return 0;
}


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

   Local Contrast function
   Rescale pixel brightness based on surrounding area mean brightness.

   loop all pixels
      B = pixel brightness value
      M = average of pixels in neighborhood (zone) 
      R = M / B
      B = 255 * POW(B/255,R)     new pixel brightness

***/


namespace localcon_names
{
   int         Zrad1, Zrad2;                                                     //  inner, outer zone radius
   float       *Zbrite1, *Zbrite2;                                               //  inner, outer zone mean brightness
   float       Fpower;                                                           //  function power, 0-1
   float       Fcolor;                                                           //  color increase, 0-1
   int         Fnewrad, Fnewpower, Fnewcolor;                                    //  flags, new inputs available
   editfunc    EFlocalcon;                                                       //  edit function struct
}


//  menu function

void m_localcon(GtkWidget *, ch  *menu)                                          //  reduce halo effects
{
   using namespace localcon_names;

   int    localcon_dialog_event(zdialog *zd, ch  *event);
   void * localcon_thread(void *);
   void   localcon_curvedit(int spc);
   
   GtkWidget   *drawwin_scale;                                                   //  drawing window for brightness scale   24.40

   F1_help_topic = "local contrast";
   
   Plog(1,"m_localcon \n");

   ch  *title = "Local Contrast";
   
   EFlocalcon.menuname = "Local Contrast";
   EFlocalcon.menufunc = m_localcon;
   EFlocalcon.Farea = 2;                                                         //  select area usable
   EFlocalcon.Fscript = 1;                                                       //  scripting supported
   EFlocalcon.Fpaintedits = 1;                                                   //  use with paint edits OK
   EFlocalcon.threadfunc = localcon_thread;                                      //  thread function

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

   int cc = Eww * Ehh * sizeof(float);
   Zbrite1 = (float *) zmalloc(cc,"localcon");                                   //  memory for brightness means, 2 zones
   Zbrite2 = (float *) zmalloc(cc,"localcon");
   
/***
          ________________________________
         |        Local Contrast          |
         |  ____________________________  |
         | |                            | |
         | |                            | |
         | | -------------------------- | |                                      //  curve edit area                       24.40
         | |                            | |
         | |____________________________| |
         | |____________________________| |                                      //  brightness scale: black to white      24.40
         |                                |
         | Power   =============[]======  |
         | Radius  ========[]===========  |
         | Brighten  ===========[]======  |
         | Color  ==========[]==========  |
         |                                |
         |               [Reset] [OK] [X] |
         |________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,"Reset","OK"," X ",null);
   EFlocalcon.zd = zd;
   
   zdialog_add_widget(zd,"frame","frameH","dialog",0,"expand");                  //  edit-curve                            24.40
   zdialog_add_widget(zd,"frame","frameB","dialog");                             //  black to white brightness scale       24.40

   zdialog_add_widget(zd,"hbox","hbpow","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labpow","hbpow","Power","space=5");
   zdialog_add_widget(zd,"hscale2","power","hbpow","0|1|0.01|0","expand");       //  power range, 0-1
   zdialog_add_widget(zd,"hbox","hbrad","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labrad","hbrad","Radius","space=5");
   zdialog_add_widget(zd,"hscale2","Zrad2","hbrad","20|100|2|10","expand");      //  Zrad2 range 20-100 pixels
   zdialog_add_widget(zd,"hbox","hbcolor","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labcolor","hbcolor","Color","space=5");
   zdialog_add_widget(zd,"hscale2","color","hbcolor","0|1|0.01|0","expand");     //  color range, 0-1

   GtkWidget *frameH = zdialog_gtkwidget(zd,"frameH");                           //  setup edit curves
   spldat *sd = splcurve_init(frameH,localcon_curvedit);
   EFlocalcon.sd = sd;

   sd->Nscale = 1;                                                               //  horizontal line, neutral value
   sd->xscale[0][0] = 0.00;
   sd->yscale[0][0] = 0.50;
   sd->xscale[1][0] = 1.00;
   sd->yscale[1][0] = 0.50;

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

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

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

   Zrad2 = 20;                                                                   //  initial values
   Zrad1 = Zrad2 / 2;
   Fnewrad = 1;
   Fpower = 0;
   Fnewpower = 0;
   Fcolor = 0;
   Fnewcolor = 0;
   
   zdialog_resize(zd,200,0);
   zdialog_run(zd,localcon_dialog_event,"save");                                 //  run dialog - parallel

   thread_signal();
   return;
}


//  dialog event and completion function

int localcon_dialog_event(zdialog *zd, ch *event)
{
   using namespace localcon_names;

   spldat   *sd = EFlocalcon.sd;
   
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  apply and quit
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  cancel
   
   if (strmatch(event,"focus")) return 1;

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

   if (strmatch(event,"Zrad2")) {                                                //  radius input
      zdialog_fetch(zd,"Zrad2",Zrad2);                                           //  outer radius
      Zrad1 = Zrad2 / 2;                                                         //  inner radius
      Fnewrad = 1;
   }

   if (strmatch(event,"power")) {                                                //  power input
      zdialog_fetch(zd,"power",Fpower);
      Fnewpower = 1;
   }

   if (strmatch(event,"color")) {                                                //  color input
      zdialog_fetch(zd,"color",Fcolor);
      Fnewcolor = 1;
   }
   
   if (strmatch(event,"paint"))                                                  //  mouse paint
      Fnewpower = 1;

   thread_signal();

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

         zdialog_stuff(zd,"power",0);
         zdialog_stuff(zd,"color",0);
         zdialog_stuff(zd,"Zrad2",20);
         Zrad2 = 20;                                                             //  initial values
         Zrad1 = Zrad2 / 2;
         Fnewrad = 1;
         Fpower = 0;
         Fnewpower = 0;
         Fcolor = 0;
         Fnewcolor = 0;

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

         gtk_widget_queue_draw(sd->drawarea);                                    //  redraw curve
        
         edit_reset();
         return 1;
      }

      else if (zd->zstat == 2)                                                   //  done - commit edit
      {
         edit_addhist("pow:%.3f rad:%d color:%.3f",Fpower,Zrad2,Fcolor);         //  edit parms > edit hist
         edit_done(0);
      }

      else edit_cancel(0);                                                       //  discard edit

      zfree(Zbrite1);                                                            //  free memory
      zfree(Zbrite2);
      return 1;
   }

   return 1;
}


//  this function is called when the curve is edited

void localcon_curvedit(int spc)                                                  //  24.40
{
   using namespace localcon_names;
   Fnewpower = 1;                                                                //  affects power calculation
   thread_signal();
   return;
}


//  main thread function

void * localcon_thread(void *)
{
   using namespace localcon_names;

   void * localcon_wthread_rad1(void *);
   void * localcon_wthread_rad2(void *);
   void * localcon_wthread_power(void *);

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

   if (! Fnewrad && ! Fnewpower && ! Fnewcolor)                                  //  no new inputs
      return 0;
   
   while (Fnewrad)                                                               //  radius was changed
   {
      progress_setgoal(Eww * Ehh * 3);                                           //  initz. progress counter
      Fnewrad = 0;
      get_edit_pixels_init(NSMP,0);                                              //  compute inner zone brightness
      do_wthreads(localcon_wthread_rad1,NSMP);
      get_edit_pixels_init(NSMP,0);                                              //  compute outer zone brightness
      do_wthreads(localcon_wthread_rad2,NSMP);
      progress_setgoal(0);
      Fnewpower++;
   }
   
   while (Fnewpower) {                                                           //  power changed
      Fnewpower = 0;
      get_edit_pixels_init(NSMP,0);
      do_wthreads(localcon_wthread_power,NSMP);
   }

   while (Fnewcolor) {                                                           //  color saturation changed
      Fnewcolor = 0;
      get_edit_pixels_init(NSMP,0);
      do_wthreads(localcon_wthread_power,NSMP);
   }

   if (! Fpaintedits) Fpaint2();

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

   return 0;
}


//  worker thread function
//  Compute mean brightness for zones around each pixel.
//  Compute for both Zrad1 (inner) and Zrad2 (outer) zones.
//  Zbrite1[*] and Zbrite2[*]: brightness for zones around pixel [*] 

void * localcon_wthread_rad1(void *arg)
{
   using namespace localcon_names;

   int      index = *((int *) arg);
   int      px1, py1, px2, py2, px3;
   int      pxlo, pxhi, pylo, pyhi;
   int      ii, step;
   float    *pix;
   double   sum = 0;
   int      count = 0;
   
   step = 1;

   for (py1 = index; py1 < Ehh; py1 += NSMP)
   for (px1 = 0; px1 < Eww; px1++)   
   {
      pylo = py1 - Zrad1;                                                        //  inner zone y-range
      if (pylo < 0) pylo = 0;
      pyhi = py1 + Zrad1;
      if (pyhi > Ehh) pyhi = Ehh;

      pxlo = px1 - Zrad1;                                                        //  inner zone x-range
      if (pxlo < 0) pxlo = 0;
      pxhi = px1 + Zrad1;
      if (pxhi > Eww) pxhi = Eww;

      if (px1 == 0) {                                                            //  start new row, first zone for row
         sum = count = 0;
         for (py2 = pylo; py2 < pyhi; py2 += step)
         for (px2 = pxlo; px2 < pxhi; px2 += step) {
            if (sa_stat == sa_stat_fini) {                                       //  exclude pixels outside select area
               ii = Eww * py2 + px2;
               if (sa_pixmap[ii] < 1) continue;
            }
            pix = PXMpix(E1pxm,px2,py2);                                         //  accumulate brightness sum
            sum += 0.001302 * (pix[0] + pix[1] + pix[2]);
            count++;
         }
         continue;                                                               //  next x column, next zone
      }
      
      if (pxlo > 0) {
         px3 = pxlo - 1;                                                         //  subtract px3 column from zone
         for (py2 = pylo; py2 < pyhi; py2 += step) {                             //  (left column removed from zone)
            if (sa_stat == sa_stat_fini) {
               ii = Eww * py2 + px3;
               if (sa_pixmap[ii] < 1) continue;
            }
            pix = PXMpix(E1pxm,px3,py2);
            sum = sum - 0.001302 * (pix[0] + pix[1] + pix[2]);
            count--;
         }
      }
      
      if (pxhi < Eww) {                                                          //  add px3 column to zone
         px3 = pxhi;                                                             //  (right column added to zone)
         for (py2 = pylo; py2 < pyhi; py2 += step) {
            if (sa_stat == sa_stat_fini) {
               ii = Eww * py2 + px3;
               if (sa_pixmap[ii] < 1) continue;
            }
            pix = PXMpix(E1pxm,px3,py2);
            sum = sum + 0.001302 * (pix[0] + pix[1] + pix[2]);
            count++;
         }
      }
      
      ii = py1 * Eww + px1;
      if (count > 0) Zbrite1[ii] = sum / count;                                  //  zone mean brightness
      else Zbrite1[ii] = 0;
      
      progress_addvalue(index,1);                                                //  track progress
   }

   return 0;                                                                     //  exit thread
}

void * localcon_wthread_rad2(void *arg)
{
   using namespace localcon_names;

   int      index = *((int *) arg);
   int      px1, py1, px2, py2, px3;
   int      pxlo, pxhi, pylo, pyhi;
   int      ii, step;
   float    *pix;
   double   sum = 0;
   int      count = 0;
   
   step = 1;
   if (Zrad2 > 50) step = 2;

   for (py1 = index; py1 < Ehh; py1 += NSMP)
   for (px1 = 0; px1 < Eww; px1++)   
   {
      pylo = py1 - Zrad2;
      if (pylo < 0) pylo = 0;
      pyhi = py1 + Zrad2;
      if (pyhi > Ehh) pyhi = Ehh;

      pxlo = px1 - Zrad2;
      if (pxlo < 0) pxlo = 0;
      pxhi = px1 + Zrad2;
      if (pxhi > Eww) pxhi = Eww;

      if (px1 == 0) {
         sum = count = 0;
         for (py2 = pylo; py2 < pyhi; py2 += step)
         for (px2 = pxlo; px2 < pxhi; px2 += step) {
            if (sa_stat == sa_stat_fini) {
               ii = Eww * py2 + px2;
               if (sa_pixmap[ii] < 1) continue;
            }
            pix = PXMpix(E1pxm,px2,py2);
            sum += 0.001302 * (pix[0] + pix[1] + pix[2]);
            count++;
         }
         continue;
      }
      
      if (pxlo > 0) {
         px3 = pxlo - 1;
         for (py2 = pylo; py2 < pyhi; py2 += step) {
            if (sa_stat == sa_stat_fini) {
               ii = Eww * py2 + px3;
               if (sa_pixmap[ii] < 1) continue;
            }
            pix = PXMpix(E1pxm,px3,py2);
            sum = sum - 0.001302 * (pix[0] + pix[1] + pix[2]);
            count--;
         }
      }
      
      if (pxhi < Eww) {
         px3 = pxhi;
         for (py2 = pylo; py2 < pyhi; py2 += step) {
            if (sa_stat == sa_stat_fini) {
               ii = Eww * py2 + px3;
               if (sa_pixmap[ii] < 1) continue;
            }
            pix = PXMpix(E1pxm,px3,py2);
            sum = sum + 0.001302 * (pix[0] + pix[1] + pix[2]);
            count++;
         }
      }
      
      ii = py1 * Eww + px1;
      if (count > 0) Zbrite2[ii] = sum / count;
      else Zbrite2[ii] = 0;
      
      progress_addvalue(index,2);
   }

   return 0;                                                                     //  exit thread
}


//  worker thread function
//  calculate new pixel brightness levels based on pixel brightness
//    compared to brightness of surrounding zones

void * localcon_wthread_power(void *arg)
{
   using namespace localcon_names;

   int      index = *((int *) arg);
   int      px1, py1, Fend, ii;
   float    Bp, Bz1, Bz2, BR, FC;
   float    R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float    *pix1, *pix3;
   float    pixbrite;
   float    blend;
   spldat   *sd = EFlocalcon.sd;
   
   while (true)
   {
      Fend = get_edit_pixels(index,px1,py1,blend);
      if (Fend) break;
      if (blend == 0) continue;

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

      R1 = pix1[0];                                                              //  input image RGB values
      G1 = pix1[1];
      B1 = pix1[2];
      
      R3 = pix3[0];                                                              //  current output image RGB
      G3 = pix3[1];
      B3 = pix3[2];

      Bp = 0.001302 * (R1 + G1 + B1);                                            //  pixel brightness, 0-1
      if (Bp == 0) continue;
      
      ii = py1 * Eww + px1;
      Bz1 = Zbrite1[ii];                                                         //  inner zone mean brightness, 0-1
      Bz2 = Zbrite2[ii];                                                         //  outer zone mean brightness, 0-1

      BR = 0.5 * (Bz1 + Bz2) / Bp;                                               //  zone / pixel brightness ratio
      BR = pow(Bp,BR*BR);                                                        //  new pixel brightness

      ii = 999 * BR;                                                             //  pixel brightness scaled 0-999         24.40
      if (ii < 1 || ii > 998) continue;
      FC = sd->yval[0][ii];                                                      //  brightness curve value 0 ... 1.0
      FC = FC * 2;                                                               //  0 ... 2.0
      BR = BR * FC;
      if (BR > 1.0) BR = 1.0;
      
      R9 = BR * R1;                                                              //  modified RGB values
      G9 = BR * G1;
      B9 = BR * B1;

      if (Fcolor) {                                                              //  + saturation
         pixbrite = 0.333 * (R9 + G9 + B9);                                      //  pixel brightness, 0-255
         R9 = R9 + Fcolor * (R9 - pixbrite);                                     //  Fcolor, 0-1
         G9 = G9 + Fcolor * (G9 - pixbrite);
         B9 = B9 + Fcolor * (B9 - pixbrite);
      }
      
      RGBfix(R9,G9,B9);

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

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

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


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

   Amplify Contrast function
   improve visibility of detail by amplifying existing contrast

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

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

namespace amplifycon_names
{
   float       *brmap1, *brmap2;
   float       *brmap4[4];
   int         Fnewmap;
   int         contrast99;
   float       amplify;
   editfunc    EFamplifycon;
}


//  menu function

void m_amplifycon(GtkWidget *, ch  *menu)
{
   using namespace amplifycon_names;
   
   void  *amplifycon_thread(void *);
   int   amplifycon_dialog_event(zdialog *zd, ch  *event);
   void  amplifycon_curvedit(int);
   void  amplifycon_initz(zdialog *zd);

   ch     *title = "Amplify Contrast";
   
   F1_help_topic = "amplify contrast";

   Plog(1,"m_amplifycon \n");

   EFamplifycon.menuname = "Amplify Contrast";
   EFamplifycon.menufunc = m_amplifycon;
   EFamplifycon.Farea = 2;                                                       //  select area usable
   EFamplifycon.Fpaintedits = 1;                                                 //  use with paint edits OK
   EFamplifycon.Fscript = 1;                                                     //  scripting supported
   EFamplifycon.threadfunc = amplifycon_thread;

   if (! edit_setup(EFamplifycon)) return;                                       //  setup: no preview, select area OK

/***
          ________________________________
         |        Amplify Contrast        |
         |  ____________________________  |
         | |                            | |
         | |                            | |
         | |    curve drawing area      | |
         | |                            | |
         | |____________________________| |
         |  low       contrast      high  |
         |                                |
         |  Amplify ========[]==========  |
         |                                |
         |                       [OK] [X] |
         |________________________________|

***/

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

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

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

   GtkWidget *frame = zdialog_gtkwidget(zd,"frame");                             //  set up curve edit
   spldat *sd = splcurve_init(frame,amplifycon_curvedit);
   EFamplifycon.sd = sd;

   sd->Nspc = 1;
   sd->fact[0] = 1;
   sd->vert[0] = 0;
   sd->nap[0] = 2;                                                               //  initial curve anchor points
   sd->apx[0][0] = 0.01;
   sd->apy[0][0] = 0.20;
   sd->apx[0][1] = 0.99;
   sd->apy[0][1] = 0.20;

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

   amplifycon_initz(zd);                                                         //  initialize brightness maps
   
   zdialog_resize(zd,250,300);
   zdialog_run(zd,amplifycon_dialog_event,"save");                               //  run dialog - parallel

   amplify = 0;
   Fnewmap = 1;
   thread_signal();

   return;
}


//  initialization for new image file
//  set brmap1[] and contrast99

void amplifycon_initz(zdialog *zd)
{
   using namespace amplifycon_names;

   int         ii, cc, px, py;
   float       b1, *pix1;
   int         jj, sum, limit, conhist[100];

   cc = Eww * Ehh * sizeof(float);                                               //  allocate brightness map memory
   brmap1 = (float *) zmalloc(cc,"amplifycon");
   brmap2 = (float *) zmalloc(cc,"amplifycon");
   for (ii = 0; ii < 4; ii++)
      brmap4[ii] = (float *) zmalloc(cc,"amplifycon");

   for (py = 0; py < Ehh; py++)                                                  //  map initial image brightness
   for (px = 0; px < Eww; px++)
   {
      ii = py * Eww + px;
      pix1 = PXMpix(E1pxm,px,py);
      b1 = 0.333 * (pix1[0] + pix1[1] + pix1[2]);                                //  pixel brightness 0-255.9
      if (b1 < 1) b1 = 1;
      brmap1[ii] = b1;
   }

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

   for (py = 1; py < Ehh; py++)                                                  //  map contrast histogram
   for (px = 1; px < Eww; px++)
   {
      ii = py * Eww + px;
      jj = 0.388 * fabsf(brmap1[ii] - brmap1[ii-1]);                             //  horiz. contrast, ranged 0 - 99
      if (jj > 99) jj = 99;                                                      //  trap bad image data
      conhist[jj]++;
      jj = 0.388 * fabsf(brmap1[ii] - brmap1[ii-Eww]);                           //  vertical
      if (jj > 99) jj = 99;
      conhist[jj]++;
   }

   sum = 0;
   limit = 0.99 * 2 * (Eww-1) * (Ehh-1);                                         //  find 99th percentile contrast

   for (ii = 0; ii < 100; ii++) {
      sum += conhist[ii];
      if (sum > limit) break;
   }

   contrast99 = 255.0 * ii / 100.0;                                              //  0 to 255
   if (contrast99 < 4) contrast99 = 4;                                           //  rescale low-contrast image

   return;
}


//  dialog event and completion callback function

int amplifycon_dialog_event(zdialog *zd, ch  *event)
{
   using namespace amplifycon_names;

   ch       text[8];

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

   if (zd->zstat) {                                                              //  dialog complete
      if (zd->zstat == 1) edit_done(0);
      else edit_cancel(0);
      zfree(brmap1);                                                             //  free memory
      zfree(brmap2);
      brmap1 = brmap2 = 0;
      for (int ii = 0; ii < 4; ii++) {
         zfree(brmap4[ii]);
         brmap4[ii] = 0;
      }
      return 1;
   }
   
   if (strmatch(event,"apply"))                                                  //  from script
      amplifycon_dialog_event(zd,"amplify");

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

   if (strmatch(event,"amplify")) {                                              //  slider value
      zdialog_fetch(zd,"amplify",amplify);
      snprintf(text,8,"%.2f",amplify);                                           //  numeric feedback
      zdialog_stuff(zd,"ampval",text);
      Fnewmap = 1;
      thread_signal();                                                           //  trigger update thread
   }

   return 1;
}


//  this function is called when the curve is edited

void amplifycon_curvedit(int)
{
   using namespace amplifycon_names;

   Fnewmap = 1;
   thread_signal();
   return;
}


//  thread function

void * amplifycon_thread(void *)
{
   using namespace amplifycon_names;
   
   void * amplifycon_wthread1(void *arg);
   void * amplifycon_wthread2(void *arg);

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

   if (Fnewmap)                                                                  //  recalculate amplifycon
      do_wthreads(amplifycon_wthread1,4);
   Fnewmap = 0;

   get_edit_pixels_init(NSMP,0);                                                 //  initz. pixel loop
   do_wthreads(amplifycon_wthread2,NSMP);                                        //  update image

   CEF->Fmods++;
   CEF->Fsaved = 0;
   if (amplify == 0) CEF->Fmods = 0;

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


//  calculate brmap2[] and brmap4[] from brmap1[], 
//    contrast curve, amplify, contrast99

void * amplifycon_wthread1(void *arg)
{
   using namespace amplifycon_names;

   int         bii;
   int         ii, kk, pii;
   int         px, px1, px2, pxinc;
   int         py, py1, py2, pyinc;
   float       b1, b4, xval, yval, grad;
   float       amp, con99;
   spldat      *sd = EFamplifycon.sd;    

   con99 = contrast99;                                                           //  99th percentile contrast
   con99 = 1.0 / con99;                                                          //  inverted
   amp = pow(amplify,0.5);                                                       //  amplification, 0 to 1

   bii = *((int *) arg);                                                         //  thread: 0/1/2/3

   if (bii == 0) {                                                               //  direction SE
      px1 = 1; 
      px2 = Eww; 
      pxinc = 1;
      py1 = 1; 
      py2 = Ehh; 
      pyinc = 1;
      pii = - 1 - Eww;
   }

   else if (bii == 1) {                                                          //  direction SW
      px1 = Eww-2; 
      px2 = 0; 
      pxinc = -1;
      py1 = 1; 
      py2 = Ehh; 
      pyinc = 1;
      pii = + 1 - Eww;
   }

   else if (bii == 2) {                                                          //  direction NE
      px1 = 1; 
      px2 = Eww; 
      pxinc = 1;
      py1 = Ehh-2; 
      py2 = 0; 
      pyinc = -1;
      pii = - 1 + Eww;
   }

   else {   /* bii == 3 */                                                       //  direction NW
      px1 = Eww-2; 
      px2 = 0; 
      pxinc = -1;
      py1 = Ehh-2; 
      py2 = 0; 
      pyinc = -1;
      pii = + 1 + Eww;
   }

   for (ii = 0; ii < Eww * Ehh; ii++)                                            //  initial brightness map
      brmap4[bii][ii] = brmap1[ii];

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

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

      xval = fabsf(grad) * con99;                                                //  gradient scaled 0 to 1+
      kk = 1000.0 * xval;
      if (kk > 999) kk = 999;
      yval = 1.0 + 5.0 * sd->yval[0][kk];                                        //  amplifier = 1...6
      grad = grad * yval;                                                        //  magnified gradient

      b4 = brmap4[bii][ii+pii] + grad;                                           //  pixel brightness = prior + gradient
      b4 = (1.0 - amp) * b1 + amp * b4;                                          //  apply amplifier: b4 range = b1 --> b4

      brmap4[bii][ii] = b4;                                                      //  new pixel brightness
   }

   for (py = bii; py < Ehh; py += 4)                                             //  loop all image pixels
   for (px = 0; px < Eww; px++)
   {
      ii = py * Eww + px;

      b4 = brmap4[0][ii] + brmap4[1][ii]                                         //  new brightness = average of four
         + brmap4[2][ii] + brmap4[3][ii];                                        //    calculated brightness surfaces
      b4 = 0.25 * b4;

      if (b4 < 1) b4 = 1;

      brmap2[ii] = b4;
   }

   return 0;
}


//  calculate new pixel brightness values from brightness maps

void * amplifycon_wthread2(void *arg)
{
   using namespace amplifycon_names;

   float       *pix1, *pix3;
   int         index, ii, px, py, Fend;
   float       b1, b2, bf;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;;
   float       blend;

   index = *((int *) arg);

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

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

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

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

      ii = py * Eww + px;
      b1 = brmap1[ii];                                                           //  initial pixel brightness
      b2 = brmap2[ii];                                                           //  calculated new brightness

      bf = b2 / b1;                                                              //  brightness ratio

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

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

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

   return 0;
}


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

//  Global Retinex function
//  Rescale RGB values based on entire image - eliminate color caste and reduce fog/haze.

namespace gretinex_names 
{
   editfunc    EFgretinex;                                                       //  edit function data
   ch          *thread_command;
   VOL int     Fcancel;

   float       Rdark, Gdark, Bdark;
   float       Rbrite, Gbrite, Bbrite;
   float       Rmpy, Gmpy, Bmpy;
   float       pRdark, pGdark, pBdark;                                           //  prior values 
   float       pRbrite, pGbrite, pBbrite;
   float       pRmpy, pGmpy, pBmpy;
   float       blend, reducebright;
   int         Fbrightpoint, Fdarkpoint;
}


//  menu function

void m_gretinex(GtkWidget *, ch  *menu)
{
   using namespace gretinex_names;

   int    gretinex_dialog_event(zdialog *zd, ch  *event);
   void   gretinex_mousefunc();
   void * gretinex_thread(void *);

   F1_help_topic = "global retinex";

   Plog(1,"m_gretinex \n");

   EFgretinex.menuname = "Global Retinex";
   EFgretinex.menufunc = m_gretinex;
   EFgretinex.Farea = 2;                                                         //  select area usable
   EFgretinex.Fscript = 1;                                                       //  scripting supported
   EFgretinex.threadfunc = gretinex_thread;                                      //  thread function
   EFgretinex.mousefunc = gretinex_mousefunc;                                    //  mouse function

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

   E9pxm = 0;
   
/***
          ______________________________________
         |           Global Retinex             |
         |                                      |
         |               Red  Green  Blue  All  |
         |  Dark Point:  [__]  [__]  [__]  [__] |        0 ... 255      neutral point is 0
         | Bright Point: [__]  [__]  [__]  [__] |        0 ... 255      neutral point is 255
         |  Multiplyer:  [__]  [__]  [__]  [__] |        0 ... 5        neutral point is 1
         |                                      |
         | [auto] brightness rescale            |
         | [x] bright point  [x] dark point     |        mouse click spots
         | blend: =======[]===================  |
         | reduce bright: ==========[]========  |
         |                                      |
         |                     [Reset] [OK] [X] |
         |______________________________________|

***/

   zdialog *zd = zdialog_new("Global Retinex",Mwin,"Reset","OK"," X ",null); 
   EFgretinex.zd = zd;
   
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","space","hb1","","space=2");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog");
   zdialog_add_widget(zd,"label","space","hb1","","space=2");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog");
   zdialog_add_widget(zd,"label","space","hb1","","space=2");
   zdialog_add_widget(zd,"vbox","vb3","hb1",0,"homog");
   zdialog_add_widget(zd,"label","space","hb1","","space=2");
   zdialog_add_widget(zd,"vbox","vb4","hb1",0,"homog");
   zdialog_add_widget(zd,"label","space","hb1","","space=2");
   zdialog_add_widget(zd,"vbox","vb5","hb1",0,"homog");
   
   zdialog_add_widget(zd,"label","space","vb1","");
   zdialog_add_widget(zd,"label","labdark","vb1","Dark Point");
   zdialog_add_widget(zd,"label","labbrite","vb1","Bright Point");
   zdialog_add_widget(zd,"label","labmpy","vb1","Multiplyer");

   zdialog_add_widget(zd,"label","labred","vb2","Red");
   zdialog_add_widget(zd,"zspin","Rdark","vb2","0|255|1|0","size=3");
   zdialog_add_widget(zd,"zspin","Rbrite","vb2","0|255|1|255","size=3");
   zdialog_add_widget(zd,"zspin","Rmpy","vb2","0.1|5|0.01|1","size=3");

   zdialog_add_widget(zd,"label","labgreen","vb3","Green");
   zdialog_add_widget(zd,"zspin","Gdark","vb3","0|255|1|0","size=3");
   zdialog_add_widget(zd,"zspin","Gbrite","vb3","0|255|1|255","size=3");
   zdialog_add_widget(zd,"zspin","Gmpy","vb3","0.1|5|0.01|1","size=3");

   zdialog_add_widget(zd,"label","labred","vb4","Blue");
   zdialog_add_widget(zd,"zspin","Bdark","vb4","0|255|1|0","size=3");
   zdialog_add_widget(zd,"zspin","Bbrite","vb4","0|255|1|255","size=3");
   zdialog_add_widget(zd,"zspin","Bmpy","vb4","0.1|5|0.01|1","size=3");

   zdialog_add_widget(zd,"label","laball","vb5","All");
   zdialog_add_widget(zd,"zspin","dark+-","vb5","-1|+1|1|0","size=3");
   zdialog_add_widget(zd,"zspin","brite+-","vb5","-1|+1|1|0","size=3");
   zdialog_add_widget(zd,"zspin","mpy+-","vb5","-1|+1|1|0","size=3");

   zdialog_add_widget(zd,"hbox","hbauto","dialog");
   zdialog_add_widget(zd,"button","autoscale","hbauto","Auto","space=3");
   zdialog_add_widget(zd,"label","labauto","hbauto","brightness rescale","space=5");

   zdialog_add_widget(zd,"hbox","hbclicks","dialog");
   zdialog_add_widget(zd,"check","brightpoint","hbclicks","click bright point","space=3");
   zdialog_add_widget(zd,"check","darkpoint","hbclicks","click dark point","space=5");
   
   zdialog_add_widget(zd,"hbox","hbb","dialog");
   zdialog_add_widget(zd,"label","labb","hbb","blend","space=5");
   zdialog_add_widget(zd,"hscale","blend","hbb","0|1.0|0.01|1.0","expand");
   zdialog_add_widget(zd,"hbox","hbrd","dialog");
   zdialog_add_widget(zd,"label","labrd","hbrd","reduce bright","space=5");
   zdialog_add_widget(zd,"hscale","reduce bright","hbrd","0|1.0|0.01|0.0","expand");

   zdialog_run(zd,gretinex_dialog_event,"save");                                 //  run dialog - parallel
   zdialog_send_event(zd,"reset");
   
   PXM_audit(E1pxm);
   return;
}


//  dialog event and completion function

int gretinex_dialog_event(zdialog *zd, ch  *event)
{
   using namespace gretinex_names;
   
   void  gretinex_mousefunc();

   int      adddark, addbrite, addmpy;
   int      Fchange = 0;
   
   if (strmatch(event,"focus")) return 1;                                        //  stop loss of button event (?)
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   if (strmatch(event,"reset")) zd->zstat = 1;                                   //  initz. 
   
   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  reset
         zd->zstat = 0;                                                          //  keep dialog active
         Rdark = Gdark = Bdark = 0;                                              //  set neutral values
         Rbrite = Gbrite = Bbrite = 255;
         pRdark = pGdark = pBdark = 0;                                           //  prior values = same
         pRbrite = pGbrite = pBbrite = 255;                                      //  (no change)
         Rmpy = Gmpy = Bmpy = 1.0;
         Fbrightpoint = Fdarkpoint = 0;
         blend = 1.0;
         reducebright = 0.0;
         Fcancel = 0;

         zdialog_stuff(zd,"Rdark",0);                                            //  stuff neutral values into dialog
         zdialog_stuff(zd,"Gdark",0);
         zdialog_stuff(zd,"Bdark",0);
         zdialog_stuff(zd,"Rbrite",255);
         zdialog_stuff(zd,"Gbrite",255);
         zdialog_stuff(zd,"Bbrite",255);
         zdialog_stuff(zd,"Rmpy",1.0);
         zdialog_stuff(zd,"Gmpy",1.0);
         zdialog_stuff(zd,"Bmpy",1.0);
         zdialog_stuff(zd,"brightpoint",0);
         zdialog_stuff(zd,"darkpoint",0);

         edit_reset();
         if (E9pxm) PXM_free(E9pxm);                                             //  free memory
         E9pxm = 0;
         return 1;
      }

      else if (zd->zstat == 2) {                                                 //  done
         edit_done(0);                                                           //  commit edit
         if (E9pxm) PXM_free(E9pxm);                                             //  free memory
         E9pxm = 0;
         return 1;
      }

      else {
         Fcancel = 1;                                                            //  kill thread
         edit_cancel(0);                                                         //  discard edit
         if (E9pxm) PXM_free(E9pxm);                                             //  free memory
         E9pxm = 0;
         return 1;
      }
   }

   Fbrightpoint = Fdarkpoint = 0;                                                //  disable mouse

   if (strmatch(event,"autoscale"))                                              //  auto set dark and bright points
   {
      edit_reset();

      thread_command = "autoscale";                                              //  get dark/bright limits from
      thread_signal();                                                           //    darkest/brightest pixels
      thread_wait();
      if (Fcancel) return 1;

      zdialog_stuff(zd,"Rdark",Rdark);                                           //  update dialog widgets
      zdialog_stuff(zd,"Gdark",Gdark);
      zdialog_stuff(zd,"Bdark",Bdark);
      zdialog_stuff(zd,"Rbrite",Rbrite);
      zdialog_stuff(zd,"Gbrite",Gbrite);
      zdialog_stuff(zd,"Bbrite",Bbrite);
      zdialog_stuff(zd,"Rmpy",1.0);
      zdialog_stuff(zd,"Gmpy",1.0);
      zdialog_stuff(zd,"Bmpy",1.0);
      zdialog_stuff(zd,"brightpoint",0);
      zdialog_stuff(zd,"darkpoint",0);

      pRdark = Rdark;                                                            //  prior values = same
      pGdark = Gdark;
      pBdark = Bdark;
      pRbrite = Rbrite; 
      pGbrite = Gbrite; 
      pBbrite = Bbrite;
      pRmpy = Rmpy;
      pGmpy = Gmpy;
      pBmpy = Bmpy;
      return 1;
   }
   
   if (strmatch(event,"brightpoint")) {
      zdialog_fetch(zd,"brightpoint",Fbrightpoint);
      if (Fbrightpoint) {
         zdialog_stuff(zd,"darkpoint",0);
         Fdarkpoint = 0;
      }
   }

   if (strmatch(event,"darkpoint")) {
      zdialog_fetch(zd,"darkpoint",Fdarkpoint);
      if (Fdarkpoint) {
         zdialog_stuff(zd,"brightpoint",0);
         Fbrightpoint = 0;
      }
   }
   
   if (zstrstr("brightpoint darkpoint",event)) {                                 //  brightpoint or darkpoint
      takeMouse(gretinex_mousefunc,dragcursor);                                  //     connect mouse function
      return 1;
   }
   else {
      zdialog_stuff(zd,"brightpoint",0);                                         //  reset zdialog checkboxes
      zdialog_stuff(zd,"darkpoint",0);
   }
   
   adddark = addbrite = addmpy = 0;
   if (strmatch(event,"dark+-")) zdialog_fetch(zd,"dark+-",adddark);             //  All button
   if (strmatch(event,"brite+-")) zdialog_fetch(zd,"brite+-",addbrite);
   if (strmatch(event,"mpy+-")) zdialog_fetch(zd,"mpy+-",addmpy);

   zdialog_stuff(zd,"dark+-",0);                                                 //  reset to zero
   zdialog_stuff(zd,"brite+-",0);
   zdialog_stuff(zd,"mpy+-",0);
   
   if (adddark) {
      zdialog_fetch(zd,"Rdark",Rdark);                                           //  increment dark levels
      zdialog_fetch(zd,"Gdark",Gdark);
      zdialog_fetch(zd,"Bdark",Bdark);
      Rdark += adddark;
      Gdark += adddark;
      Bdark += adddark;
      if (Rdark < 0) Rdark = 0;
      if (Gdark < 0) Gdark = 0;
      if (Bdark < 0) Bdark = 0;
      if (Rdark > 255) Rdark = 255;
      if (Gdark > 255) Gdark = 255;
      if (Bdark > 255) Bdark = 255;
      zdialog_stuff(zd,"Rdark",Rdark);
      zdialog_stuff(zd,"Gdark",Gdark);
      zdialog_stuff(zd,"Bdark",Bdark);
   }

   if (addbrite) {                                                               //  increment bright levels
      zdialog_fetch(zd,"Rbrite",Rbrite);
      zdialog_fetch(zd,"Gbrite",Gbrite);
      zdialog_fetch(zd,"Bbrite",Bbrite);
      Rbrite += addbrite;
      Gbrite += addbrite;
      Bbrite += addbrite;
      if (Rbrite < 0) Rbrite = 0;
      if (Gbrite < 0) Gbrite = 0;
      if (Bbrite < 0) Bbrite = 0;
      if (Rbrite > 255) Rbrite = 255;
      if (Gbrite > 255) Gbrite = 255;
      if (Bbrite > 255) Bbrite = 255;
      zdialog_stuff(zd,"Rbrite",Rbrite);
      zdialog_stuff(zd,"Gbrite",Gbrite);
      zdialog_stuff(zd,"Bbrite",Bbrite);
   }

   if (addmpy) {                                                                 //  increment mpy factors
      zdialog_fetch(zd,"Rmpy",Rmpy);
      zdialog_fetch(zd,"Gmpy",Gmpy);
      zdialog_fetch(zd,"Bmpy",Bmpy);
      Rmpy += 0.01 * addmpy;
      Gmpy += 0.01 * addmpy;
      Bmpy += 0.01 * addmpy;
      if (Rmpy < 0.1) Rmpy = 0.1;
      if (Gmpy < 0.1) Gmpy = 0.1;
      if (Bmpy < 0.1) Bmpy = 0.1;
      if (Rmpy > 5) Rmpy = 5;
      if (Gmpy > 5) Gmpy = 5;
      if (Bmpy > 5) Bmpy = 5;
      zdialog_stuff(zd,"Rmpy",Rmpy);
      zdialog_stuff(zd,"Gmpy",Gmpy);
      zdialog_stuff(zd,"Bmpy",Bmpy);
   }
   
   zdialog_fetch(zd,"Rdark",Rdark);                                              //  get all params
   zdialog_fetch(zd,"Gdark",Gdark);
   zdialog_fetch(zd,"Bdark",Bdark);
   zdialog_fetch(zd,"Rbrite",Rbrite);
   zdialog_fetch(zd,"Gbrite",Gbrite);
   zdialog_fetch(zd,"Bbrite",Bbrite);
   zdialog_fetch(zd,"Rmpy",Rmpy);
   zdialog_fetch(zd,"Gmpy",Gmpy);
   zdialog_fetch(zd,"Bmpy",Bmpy);
   
   Fchange = 0;
   if (Rdark != pRdark) Fchange = 1;                                             //  detect changed params
   if (Gdark != pGdark) Fchange = 1;
   if (Bdark != pBdark) Fchange = 1;
   if (Rbrite != pRbrite) Fchange = 1;
   if (Gbrite != pGbrite) Fchange = 1;
   if (Bbrite != pBbrite) Fchange = 1;
   if (Rmpy != pRmpy) Fchange = 1;
   if (Gmpy != pGmpy) Fchange = 1;
   if (Bmpy != pBmpy) Fchange = 1;
   
   pRdark = Rdark;                                                               //  remember values for change detection
   pGdark = Gdark;
   pBdark = Bdark;
   pRbrite = Rbrite; 
   pGbrite = Gbrite; 
   pBbrite = Bbrite; 
   pRmpy = Rmpy; 
   pGmpy = Gmpy; 
   pBmpy = Bmpy; 

   if (Fchange) {                                                                //  global params changed
      thread_command = "global";
      thread_signal();                                                           //  update image
      Fchange = 0;
   }
   
   if (zstrstr("blend, reduce bright",event)) {                                  //  blend params changed
      zdialog_fetch(zd,"blend",blend);
      zdialog_fetch(zd,"reduce bright",reducebright);
      thread_command = "blend";
      thread_signal();                                                           //  update image
   }

   return 1;
}


//  get dark point or bright point from mouse click position

void gretinex_mousefunc()
{
   using namespace gretinex_names;
   
   int         px, py, dx, dy;
   float       red, green, blue;
   float       *ppix;
   ch          mousetext[60];
   zdialog     *zd = EFgretinex.zd;
   
   if (! zd) {
      freeMouse();
      return;
   }

   if (! Fbrightpoint && ! Fdarkpoint) {
      freeMouse();
      return;
   }

   if (! LMclick) return;
   LMclick = 0;

   px = Mxclick;                                                                 //  mouse click position
   py = Myclick;

   if (px < 2) px = 2;                                                           //  pull back from edge
   if (px > Eww-3) px = Eww-3;
   if (py < 2) py = 2;
   if (py > Ehh-3) py = Ehh-3;

   red = green = blue = 0;

   for (dy = -1; dy <= 1; dy++)                                                  //  3x3 block around mouse position
   for (dx = -1; dx <= 1; dx++)
   {
      ppix = PXMpix(E1pxm,px+dx,py+dy);                                          //  input image
      red += ppix[0];
      green += ppix[1];
      blue += ppix[2];
   }

   red = red / 9.0;                                                              //  mean RGB levels
   green = green / 9.0;
   blue = blue / 9.0;

   snprintf(mousetext,60,"3x3 pixels RGB: %.0f %.0f %.0f \n",red,green,blue);
   poptext_mouse(mousetext,10,10,0,3);
   
   if (Fbrightpoint) {                                                           //  click pixel is new bright point
      Rbrite= red;
      Gbrite = green;
      Bbrite = blue;
      zdialog_stuff(zd,"Rbrite",Rbrite);                                         //  stuff values into dialog
      zdialog_stuff(zd,"Gbrite",Gbrite);
      zdialog_stuff(zd,"Bbrite",Bbrite);
   }

   if (Fdarkpoint) {                                                             //  click pixel is new dark point
      Rdark = red;
      Gdark = green;
      Bdark = blue;
      zdialog_stuff(zd,"Rdark",Rdark);                                           //  stuff values into dialog
      zdialog_stuff(zd,"Gdark",Gdark);
      zdialog_stuff(zd,"Bdark",Bdark);
   }
   
   if (Fbrightpoint || Fdarkpoint) {
      thread_command = "global";
      thread_signal();                                                           //  trigger image update
      thread_wait();     
   }
   
   return;
}


//  thread function - multiple working threads to update image

void * gretinex_thread(void *)
{
   using namespace gretinex_names;

   void  * gretinex_wthread1(void *arg);                                         //  worker threads
   void  * gretinex_wthread2(void *arg);
   void  * gretinex_wthread3(void *arg);
   
   if (strmatch(thread_command,"autoscale")) {
      do_wthreads(gretinex_wthread1,1);                                          //  worker thread for autoscale
      thread_command = "global";
   }
   
   if (strmatch(thread_command,"global"))
   {
      do_wthreads(gretinex_wthread2,NSMP);                                       //  worker thread for image RGB rescale

      if (E9pxm) PXM_free(E9pxm);                                                //  E9 image = global retinex output
      E9pxm = PXM_copy(E3pxm);

      thread_command = "blend";                                                  //  auto blend after global retinex
   }
   
   if (strmatch(thread_command,"blend"))
   {
      if (! E9pxm) E9pxm = PXM_copy(E3pxm);                                      //  stop thread crash
      do_wthreads(gretinex_wthread3,NSMP);                                       //  worker thread for image blend
   }

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

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


//  worker thread function - autoscale retinex
//  find dark and bright points

void * gretinex_wthread1(void *)
{
   using namespace gretinex_names;
   
   int      ii, dist = 0;
   int      px, py, dx, dy;
   int      red, green, blue;
   float    *pix1;

   Rdark = Gdark = Bdark = 255;
   Rbrite = Gbrite = Bbrite = 0;
   Rmpy = Gmpy = Bmpy = 1.0;
   Fbrightpoint = Fdarkpoint = 0;

   for (py = 1; py < Ehh-1; py++)                                                //  loop all pixels
   for (px = 1; px < Eww-1; px++)
   {
      if (Fcancel) break;                                                        //  user kill

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

      red = green = blue = 0;

      for (dy = -1; dy <= 1; dy++)                                               //  loop 3x3 block
      for (dx = -1; dx <= 1; dx++)
      {
         pix1 = PXMpix(E1pxm,px+dx,py+dy);                                       //  input image
         red += pix1[0];
         green += pix1[1];
         blue += pix1[2];
      }

      red = red / 9.0;                                                           //  mean RGB levels
      green = green / 9.0;
      blue = blue / 9.0;
      
      if (red < Rdark) Rdark = red;                                              //  update limits
      if (green < Gdark) Gdark = green;
      if (blue < Bdark) Bdark = blue;
      if (red > Rbrite) Rbrite = red;
      if (green > Gbrite) Gbrite = green;
      if (blue > Bbrite) Bbrite = blue;
   }
   
   Plog(2,"dark RGB: %.0f %.0f %.0f   bright RGB: %.0f %.0f %.0f \n",
            Rdark, Gdark, Bdark, Rbrite, Gbrite, Bbrite);

   return 0;
}


//  worker thread function - scale image RGB values

void * gretinex_wthread2(void *arg)
{
   using namespace gretinex_names;
   
   int      ii, index, dist = 0;
   int      px, py;
   float    R1, G1, B1, R3, G3, B3;
   float    *pix1, *pix3;

   index = *((int *) arg);
   
   for (py = index; py < Ehh; py += NSMP)                                        //  loop all image pixels
   for (px = 0; px < Eww; px++)
   {
      if (Fcancel) break;                                                        //  user kill

      if (sa_stat == sa_stat_fini) {                                             //  select area active
         ii = py * Eww + 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
      G1 = pix1[1];
      B1 = pix1[2];
      
      R1 = 255 * (R1 - Rdark) / (Rbrite - Rdark);                                //  rescale for full 0-255 range
      G1 = 255 * (G1 - Gdark) / (Gbrite - Gdark);
      B1 = 255 * (B1 - Bdark) / (Bbrite - Bdark);

      if (R1 < 0) R1 = 0;
      if (G1 < 0) G1 = 0;
      if (B1 < 0) B1 = 0;

      R3 = R1 * Rmpy;
      G3 = G1 * Gmpy;
      B3 = B1 * Bmpy;

      RGBfix(R3,G3,B3);

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

   return 0;
}


//  worker thread function - blend input and output images, attenuate bright pixels

void * gretinex_wthread3(void *arg)
{
   using namespace gretinex_names;

   int      index = *((int *) arg);
   int      px, py;
   int      ii, dist = 0;
   float    *pix1, *pix2, *pix3;
   float    R1, G1, B1, R2, G2, B2, R3, G3, B3;
   float    F1, F2, Lmax;
   
   for (py = index; py < Ehh; py += NSMP)                                        //  loop all image pixels
   for (px = 0; px < Eww; px++) 
   {
      if (Fcancel) break;                                                        //  user kill

      if (sa_stat == sa_stat_fini) {                                             //  select area active
         ii = py * Eww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside area
      }
      
      pix1 = PXMpix(E1pxm,px,py);                                                //  input image pixel
      pix2 = PXMpix(E9pxm,px,py);                                                //  global retinex image pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output image pixel

      R1 = pix1[0];                                                              //  input color - original image
      G1 = pix1[1];
      B1 = pix1[2];

      R2 = pix2[0];                                                              //  input color - retinex image
      G2 = pix2[1];
      B2 = pix2[2];

      Lmax = R1;                                                                 //  max. RGB input
      if (G1 > Lmax) Lmax = G1;
      if (B1 > Lmax) Lmax = B1;
      
      F1 = blend;                                                                //  0 ... 1  >>  E1 ... E3
      F2 = reducebright;                                                         //  0 ... 1
      F1 = F1 * (1.0 - F2 * Lmax / 255.0);                                       //  reduce F1 for high F2 * Lmax

      R3 = F1 * R2 + (1.0 - F1) * R1;                                            //  output RGB = blended inputs
      G3 = F1 * G2 + (1.0 - F1) * G1;
      B3 = F1 * B2 + (1.0 - F1) * B1;
      
      pix3[0] = R3;                                                              //  output RGB values
      pix3[1] = G3;
      pix3[2] = B3;
   }

   return 0;
}


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

//  Local Retinex function
//  Rescale RGB values within local areas: increase local contrast and color.

namespace lretinex_names 
{
   editfunc    EFlretinex;                                                       //  edit function data
   ch          *thread_command;
   float       imageblend, reducedark, reducebright;
   
   int         radius;                                                           //  user radius input (zone radius)
   int         maxzones = 10000;
   int         Nzones = 100;                                                     //  zone count, 1-2000
   int         zsize, zrows, zcols, zww, zhh;                                    //  zone data

   typedef struct {                                                              //  zone structure
      int      cx, cy;                                                           //  zone center in image
      float    minR, minG, minB;                                                 //  RGB minimum values in zone
      float    maxR, maxG, maxB;                                                 //  RGB mazimum values in zone
   }           zone_t;

   zone_t      *zones = 0;                                                       //  up to 2000 zones
   int16       *zoneindex = 0;                                                   //  zoneindex[ii][z] pixel ii, 9 zones
   int16       *zoneweight = 0;                                                  //  zoneweight[ii][z] zone weights
}


//  menu function

void m_lretinex(GtkWidget *, ch  *menu)
{
   using namespace lretinex_names;

   int    lretinex_dialog_event(zdialog *zd, ch  *event);
   void * lretinex_thread(void *);

   F1_help_topic = "local retinex";

   Plog(1,"m_lretinex \n");

   EFlretinex.menuname = "Local Retinex";
   EFlretinex.menufunc = m_lretinex;
   EFlretinex.Farea = 2;                                                         //  select area usable
   EFlretinex.Fscript = 1;                                                       //  scripting supported
   EFlretinex.Fpaintedits = 1;                                                   //  use with paint edits OK 
   EFlretinex.threadfunc = lretinex_thread;                                      //  thread function

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

   E9pxm = PXM_copy(E1pxm);
   
/***
          ______________________________________
         |            Local Retinex             |
         |                                      |
         | radius: [___]  [apply]               |
         | image blend: =======[]=============  |
         | reduce dark: ==========[]==========  |
         | reduce bright: ==========[]========  |
         |                                      |
         |                     [Reset] [OK] [X] |
         |______________________________________|

***/

   zdialog *zd = zdialog_new("Local Retinex",Mwin,"Reset","OK"," X ",null); 
   EFlretinex.zd = zd;
   
   zdialog_add_widget(zd,"hbox","hbr","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labr","hbr","radius:","space=5");
   zdialog_add_widget(zd,"zspin","radius","hbr","20|999|1|50");                  //  radius range 20-999 
   zdialog_add_widget(zd,"button","newrad","hbr","apply","space=3");
   zdialog_add_widget(zd,"hbox","hbb","dialog");
   zdialog_add_widget(zd,"label","labb","hbb","image blend","space=5");
   zdialog_add_widget(zd,"hscale2","image blend","hbb","0|1.0|0.01|0.5","expand");
   zdialog_add_widget(zd,"hbox","hbrd","dialog");
   zdialog_add_widget(zd,"label","labrd","hbrd","reduce dark","space=5");
   zdialog_add_widget(zd,"hscale2","reduce dark","hbrd","0|1.0|0.01|0.0","expand");
   zdialog_add_widget(zd,"hbox","hbrl","dialog");
   zdialog_add_widget(zd,"label","labrd","hbrl","reduce bright","space=5");
   zdialog_add_widget(zd,"hscale2","reduce bright","hbrl","0|1.0|0.01|0.0","expand");

   zdialog_run(zd,lretinex_dialog_event,"save");                                 //  run dialog - parallel
   zdialog_send_event(zd,"reset");
   zdialog_send_event(zd,"newrad");
   return;
}


//  dialog event and completion function

int lretinex_dialog_event(zdialog *zd, ch  *event)
{
   using namespace lretinex_names;

   int lretinex_zonesetup(zdialog *zd);
   
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  OK, edit_setup(), f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   if (strmatch(event,"reset")) zd->zstat = 1;                                   //  initz. 
   
   if (strmatch(event,"focus")) return 1;
   
   if (strmatch(event,"apply"))                                                  //  from script                           24.30
   {
      zdialog_fetch(zd,"radius",radius);                                         //  get radius
      zdialog_fetch(zd,"image blend",imageblend);
      zdialog_fetch(zd,"reduce dark",reducedark);
      zdialog_fetch(zd,"reduce bright",reducebright);
      if (! lretinex_zonesetup(zd)) {                                            //  setup zones
         edit_cancel(0);                                                         //  failed (insufficient memory)
         PXM_free(E9pxm);
         return 1;
      }
      thread_command = "newrad";
      thread_signal();                                                           //  update image
      thread_wait();     
      zd->zstat = 2;
   }
   
   if (strmatch(event,"newrad")) {                                               //  new radius
      zdialog_fetch(zd,"radius",radius);                                         //  get radius
      if (! lretinex_zonesetup(zd)) {                                            //  setup zones
         edit_cancel(0);                                                         //  failed (insufficient memory)
         PXM_free(E9pxm);
         return 1;
      }

      thread_command = "newrad";
      thread_signal();                                                           //  update image
      thread_wait();     
   }

   if (strmatch(event,"image blend")) {                                          //  image blend param changed
      zdialog_fetch(zd,"image blend",imageblend);
      thread_command = "image blend";
      thread_signal();                                                           //  update image
      thread_wait();     
   }

   if (zstrstr(event,"reduce")) {                                                //  reduce param changed
      zdialog_fetch(zd,"reduce dark",reducedark);
      zdialog_fetch(zd,"reduce bright",reducebright);
      thread_command = "image blend";
      thread_signal();                                                           //  update image
      thread_wait();     
   }

   if (strmatch(event,"paint")) {                                                //  mouse paint
      thread_command = "image blend";
      thread_signal();                                                           //  update image
      thread_wait();     
   }

   if (zd->zstat) 
   {
      if (zd->zstat == 1) {                                                      //  reset
         zd->zstat = 0;                                                          //  keep dialog active
         imageblend = 0.5;
         reducedark = 0.0;
         reducebright = 0.0;
         zdialog_stuff(zd,"image blend",imageblend);
         zdialog_stuff(zd,"reduce dark",reducedark);
         zdialog_stuff(zd,"reduce bright",reducebright);
         edit_reset();
         PXM_free(E9pxm);
         E9pxm = PXM_copy(E1pxm);
         return 1;
      }

      else if (zd->zstat == 2) {                                                 //  done
         edit_addhist("radius:%d blend:%.3f red.D:%.3f red.B:%.3f",              //  edit parms > edit hist
                              radius,imageblend,reducedark,reducebright);
         edit_done(0);                                                           //  commit edit
         PXM_free(E9pxm);
         if (zones) zfree(zones);
         if (zoneindex) zfree(zoneindex);
         if (zoneweight) zfree(zoneweight);
         zones = 0;
         zoneindex = 0;
         zoneweight = 0;
         return 1;
      }

      else {
         edit_cancel(0);                                                         //  discard edit
         PXM_free(E9pxm);
         if (zones) zfree(zones);
         if (zoneindex) zfree(zoneindex);
         if (zoneweight) zfree(zoneweight);
         zones = 0;
         zoneindex = 0;
         zoneweight = 0;
         return 1;
      }
   }

   return 1;
}


//  setup new zone parameters and allocate memory

int lretinex_zonesetup(zdialog *zd)
{
   using namespace lretinex_names;

   int      goal, dirc, Pzones;
   int      row, col, xlo, ylo;
   int      ii, cx, cy;
   int64    nn, z64 = 1;
   size_t   reqcc;
   
   Nzones = (Eww * Ehh) / (PI * radius * radius);                                //  approx. zone count
   if (Nzones > maxzones) Nzones = maxzones;
   Pzones = Nzones - 1;

   goal = Nzones;                                                                //  new zone count goal
   dirc = Nzones - Pzones;                                                       //  direction of change
   if (dirc == 0) dirc = 1;

   while (true)
   {
      zsize = sqrt(Eww * Ehh / goal);                                            //  approx. zone size
      zrows = Ehh / zsize;                                                       //  get appropriate rows and cols
      zcols = Eww / zsize;
      if (zrows < 1) zrows = 1;
      if (zcols < 1) zcols = 1;
      Nzones = zrows * zcols;                                                    //  matching rows x cols

      if (dirc > 0 && Nzones <= Pzones) {                                        //  no increase, try again
         if (Nzones >= maxzones) break;
         goal++;
         continue;
      }

      if (dirc < 0 && Nzones >= Pzones) {                                        //  no decrease, try again
         if (Nzones == 1) break;
         goal--;
         continue;
      }

      if (dirc > 0 && Nzones > Pzones) break;                                    //  done
      if (dirc < 0 && Nzones < Pzones) break;
   }

   if (zones) zfree(zones);                                                      //  free memory
   if (zoneindex) zfree(zoneindex);
   if (zoneweight) zfree(zoneweight);
   zones = 0;
   zoneindex = 0;
   zoneweight = 0;
   
   zww = Eww / zcols;                                                            //  zone width, height
   zhh = Ehh / zrows;
   nn = z64 * Eww * Ehh * 9;                                                     //  9 neighbor zones per pixel

   reqcc = Nzones * sizeof(zone_t) + 2 * nn * sizeof(int16);                     //  check large memory is available
   if (! zmalloc_test(reqcc)) {
      reqcc = reqcc / MEGA;
      zmessageACK(Mwin,"cannot allocate %d MB memory",reqcc);
      return 0;
   }

   zones = (zone_t *) zmalloc(Nzones * sizeof(zone_t),"local retx");             //  allocate zone memory
   zoneindex = (int16 *) zmalloc(nn * sizeof(int16),"local retx");               //  allocate pixel zone index
   zoneweight = (int16 *) zmalloc(nn * sizeof(int16),"local retx");              //  allocate pixel zone weight

   for (row = 0; row < zrows; row++)                                             //  loop all zones
   for (col = 0; col < zcols; col++)
   {
      xlo = col * zww;                                                           //  zone low pixel
      ylo = row * zhh;
      cx = xlo + zww/2;                                                          //  zone center pixel
      cy = ylo + zhh/2;
      ii = row * zcols + col;                                                    //  zone index
      zones[ii].cx = cx;                                                         //  zone center
      zones[ii].cy = cy;
   }

   return 1;
}


//  thread function - multiple working threads to update image

void * lretinex_thread(void *)
{
   using namespace lretinex_names;

   void * lretinex_wthread1(void *arg);
   void * lretinex_wthread2(void *arg);
   void * lretinex_wthread3(void *arg);
   
   if (strmatch(thread_command,"newrad"))                                        //  update zones and image
   {
      do_wthreads(lretinex_wthread1,NSMP);                                       //  compute zone RGB levels and weights
      do_wthreads(lretinex_wthread2,NSMP);                                       //  compute new pixel RGB values
      thread_command = "image blend";                                            //  auto blend after
   }
   
   if (Fpaintedits && ! Mdrag) return 0;                                         //  if mouse paint, must be drag

   if (strmatch(thread_command,"image blend")) {
      get_edit_pixels_init(NSMP,0);                                              //  initz. pixel loop
      do_wthreads(lretinex_wthread3,NSMP);                                       //  blend input and retinex images
   }

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

   if (! Fpaintedits) Fpaint2();                                                 //  update window

   return 0;
}


//  worker thread to compute zone RGB levels and weights per image pixel

void * lretinex_wthread1(void *arg)
{
   using namespace lretinex_names;

   int      index = *((int *) arg);
   int      px, py, row, col;
   int      xlo, xhi, ylo, yhi, cx, cy;
   int      ii, jj, kk;
   int64    zii, z64 = 1;
   float    minR, minG, minB, maxR, maxG, maxB;
   float    dist, maxdist, weight, sumweight;
   float    *pix;

   for (row = index; row < zrows; row += NSMP)                                   //  loop all zones
   for (col = 0; col < zcols; col++)
   {
      xlo = col * zww;                                                           //  zone low pixel
      ylo = row * zhh;
      xhi = xlo + zww;                                                           //  zone high pixel
      yhi = ylo + zhh;

      minR = minG = minB = 256;
      maxR = maxG = maxB = 0;

      for (py = ylo; py < yhi; py++)                                             //  find min/max RGB in zone
      for (px = xlo; px < xhi; px++)
      {
         pix = PXMpix(E1pxm,px,py);
         if (pix[0] < minR) minR = pix[0];
         if (pix[1] < minG) minG = pix[1];
         if (pix[2] < minB) minB = pix[2];
         if (pix[0] > maxR) maxR = pix[0];
         if (pix[1] > maxG) maxG = pix[1];
         if (pix[2] > maxB) maxB = pix[2];
      }
      
      ii = row * zcols + col;                                                    //  zone index
      zones[ii].minR = minR;                                                     //  zone RGB range
      zones[ii].minG = minG;
      zones[ii].minB = minB;
      zones[ii].maxR = maxR;
      zones[ii].maxG = maxG;
      zones[ii].maxB = maxB;
   }

   for (py = index; py < Ehh; py += NSMP)                                        //  loop all image pixels
   for (px = 0; px < Eww; px++)
   {
      row = py / zhh;                                                            //  zone containing pixel
      col = px / zww;

      zii = z64 * (py * Eww + px) * 9;                                           //  base zone index for pixel
      
      for (jj = row-1; jj <= row+1; jj++)                                        //  loop 9 neighboring zones
      for (kk = col-1; kk <= col+1; kk++)
      {
         if (jj >= 0 && jj < zrows && kk >= 0 && kk < zcols)                     //  neighbor zone number
            zoneindex[zii] = jj * zcols + kk;                                    //    --> pixel zone index
         else zoneindex[zii] = -1;                                               //  pixel zone on edge, missing neighbor
         zii++;
      }
   }
   
   if (zww < zhh) maxdist = 1.5 * zww;                                           //  based on 3x3 zones
   else maxdist = 1.5 * zhh;

   for (py = index; py < Ehh; py += NSMP)                                        //  loop all image pixels
   for (px = 0; px < Eww; px++)
   {
      row = py / zhh;                                                            //  zone containing pixel
      col = px / zww;

      zii = z64 * (py * Eww + px) * 9;                                           //  base zone index for pixel
      
      for (jj = 0; jj < 9; jj++)                                                 //  loop neighbor zones
      {
         kk = zoneindex[zii+jj];
         if (kk < 0) {                                                           //  neighbor missing 
            zoneweight[zii+jj] = 0;
            continue;
         }
         cx = zones[kk].cx;                                                      //  zone center
         cy = zones[kk].cy;
         dist = sqrtf((px-cx)*(px-cx) + (py-cy)*(py-cy));                        //  distance from (px,py)
         weight = 1.0 - dist / maxdist;                                          //  dist 0..max --> weight 1..0
         if (weight < 0) weight = 0;
         zoneweight[zii+jj] = 1000.0 * weight;                                   //  scale 1.0 = 1000
      }

      sumweight = 0;
      for (jj = 0; jj < 9; jj++)                                                 //  get sum of zone weights
         sumweight += zoneweight[zii+jj];
      
      for (jj = 0; jj < 9; jj++)                                                 //  make weights add up to 1.0
         zoneweight[zii+jj] = 1000.0 * zoneweight[zii+jj] / sumweight;           //  1000 = 1.0
   }

   return 0;
}


//  worker thread to compute new image RGB values

void * lretinex_wthread2(void *arg)
{
   using namespace lretinex_names;

   int      index = *((int *) arg);
   int      px, py, ii, jj, kk, dist = 0;
   int64    zii, z64 = 1;
   float    minR, minG, minB, maxR, maxG, maxB;
   float    R1, G1, B1, R2, G2, B2;
   float    weight;
   float    *pix1, *pix2;
   
   for (py = index; py < Ehh; py += NSMP)                                        //  loop all image pixels
   for (px = 0; px < Eww; px++)
   {
      if (sa_stat == sa_stat_fini) {                                             //  select area active
         ii = py * Eww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside area
      }

      minR = minG = minB = 0;
      maxR = maxG = maxB = 0;
      
      zii = z64 * (py * Eww + px) * 9;                                           //  base zone index for pixel
      
      for (jj = 0; jj < 9; jj++)                                                 //  loop neighbor zones
      {
         kk = zoneindex[zii+jj];                                                 //  zone number
         if (kk < 0) continue;
         weight = 0.001 * zoneweight[zii+jj];                                    //  zone weight (1000 = 1.0)
         minR += weight * zones[kk].minR;                                        //  sum weighted RGB range
         minG += weight * zones[kk].minG;                                        //    for neighbor zones
         minB += weight * zones[kk].minB;
         maxR += weight * zones[kk].maxR;
         maxG += weight * zones[kk].maxG;
         maxB += weight * zones[kk].maxB;
      }
      
      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix2 = PXMpix(E9pxm,px,py);                                                //  output pixel

      R1 = pix1[0];
      G1 = pix1[1];
      B1 = pix1[2];
      
      R2 = 255 * (R1 - minR) / (maxR - minR + 1);                                //  avoid poss. divide by zero
      G2 = 255 * (G1 - minG) / (maxG - minG + 1);
      B2 = 255 * (B1 - minB) / (maxB - minB + 1);

      RGBfix(R2,G2,B2);

      pix2[0] = R2;
      pix2[1] = G2;
      pix2[2] = B2;
   }

   return 0;
}


//  worker thread to blend input image with retinex image
//  and attenuate bright pixels

void * lretinex_wthread3(void *arg)
{
   using namespace lretinex_names;

   int      index = *((int *) arg);
   int      px, py;
   int      Fend;
   float    *pix1, *pix2, *pix3;
   float    R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float    F1, F2, Lmax, blend;

   while (true)
   {
      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) break;
      if (blend == 0) continue;
   
      pix1 = PXMpix(E1pxm,px,py);                                                //  input image pixel
      pix2 = PXMpix(E9pxm,px,py);                                                //  retinex image pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output image pixel

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

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

      R9 = pix2[0];                                                              //  retinex RGB
      G9 = pix2[1];
      B9 = pix2[2];

      Lmax = R1;                                                                 //  max. RGB input
      if (G1 > Lmax) Lmax = G1;
      if (B1 > Lmax) Lmax = B1;
      Lmax = Lmax / 255.0;                                                       //  scale 0-1
      
      F1 = imageblend;                                                           //  0 ... 1  >>  E1 ... E3

      F2 = reducedark;
      F1 = F1 * (1.0 - F2 * (1.0 - Lmax));                                       //  reduce F1 for high F2 * (1 - Lmax)

      F2 = reducebright;                                                         //  0 ... 1
      F1 = F1 * (1.0 - F2 * Lmax);                                               //  reduce F1 for high F2 * Lmax

      R9 = F1 * R9 + (1.0 - F1) * R1;                                            //  output RGB = blended inputs
      G9 = F1 * G9 + (1.0 - F1) * G1;
      B9 = F1 * B9 + (1.0 - F1) * B1;

      RGBfix(R9,G9,B9);

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

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

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

   return 0;
}


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

//  Saturation
//  Adjust color saturation for entire image (slider),
//  or set saturation based on pixel brightness (curve).

namespace saturation_names
{
   editfunc    EFsaturation;
   float       colorsat;                                                         //  saturation input, -1 ... +1
}


//  menu function

void m_saturation(GtkWidget *, ch *menu)
{
   using namespace saturation_names;

   int    saturation_dialog_event(zdialog* zd, ch  *event);
   void   saturation_curvedit(int spc);
   void * saturation_thread(void *);

   GtkWidget   *drawwin_scale;

   F1_help_topic = "saturation";

   Plog(1,"m_saturation \n");

   EFsaturation.menuname = "Saturation";
   EFsaturation.menufunc = m_saturation;
   EFsaturation.FprevReq = 1;                                                    //  use preview
   EFsaturation.Farea = 2;                                                       //  select area usable
   EFsaturation.Fpaintedits = 1;                                                 //  use with paint edits OK
   EFsaturation.Fscript = 1;                                                     //  scripting supported
   EFsaturation.threadfunc = saturation_thread;

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

/***
       _______________________________________________
      |                 Saturation                    |
      |  ___________________________________________  |
      | |                                           | |
      | |             curve edit area               | |
      | |    saturation by brightness level         | |
      | |                                           | |
      | |                                           | |
      | | ----------------------------------------- | |                          //  editable function-line
      | |                                           | |
      | |                                           | |
      | |                                           | |
      | |___________________________________________| |
      | |___________________________________________| |                          //  brightness scale: black --> white
      |                                               |
      |  Saturation  =================[]============  |                          //  saturation, B/W ... max-color
      |                                               |
      |                              [Reset] [OK] [X] |
      |_______________________________________________|

***/

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

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

   zdialog_add_widget(zd,"hbox","hbsat","dialog",0,"space=5|expand");
   zdialog_add_widget(zd,"label","labsat","hbsat","Saturation");
   zdialog_add_widget(zd,"hscale","colorsat","hbsat","-1.0|1.0|0.01|0.0","expand");

   GtkWidget *frameH = zdialog_gtkwidget(zd,"frameH");                           //  setup edit curves
   spldat *sd = splcurve_init(frameH,saturation_curvedit);
   EFsaturation.sd = sd;

   sd->Nscale = 1;                                                               //  horizontal line, neutral value
   sd->xscale[0][0] = 0.00;
   sd->yscale[0][0] = 0.50;
   sd->xscale[1][0] = 1.00;
   sd->yscale[1][0] = 0.50;

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

   sd->Nspc = 1;                                                                 //  1 curve
   sd->fact[0] = 1;                                                              //  curve 0 active 
   
   GtkWidget *frameB = zdialog_gtkwidget(zd,"frameB");                           //  setup brightness scale drawing area
   drawwin_scale = gtk_drawing_area_new();
   gtk_container_add(GTK_CONTAINER(frameB),drawwin_scale);
   gtk_widget_set_size_request(drawwin_scale,100,12);
   G_SIGNAL(drawwin_scale,"draw",brightness_scale,0);

   colorsat = 0;                                                                 //  neutral saturation

   zdialog_resize(zd,350,300);
   zdialog_run(zd,saturation_dialog_event,"save");                               //  run dialog - parallel

   return;
}


//  dialog event and completion callback function

int saturation_dialog_event(zdialog *zd, ch  *event)
{
   using namespace saturation_names;

   spldat      *sd = EFsaturation.sd;    
   float       sat0, dsat, dy;
   float       Fapply = 0;
   int         ii;
   
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  apply and quit
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  cancel
   if (strmatch(event,"apply")) Fapply = 1;                                      //  from script

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

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

      zdialog_stuff(zd,"colorsat",0);                                            //  neutral saturation
      colorsat = 0;

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

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

      edit_reset();
      return 1;
   }

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

      else edit_cancel(0);                                                       //  no change
      return 1;
   }
   
   if (zd->zstat < 0 || zd->zstat > 2) {                                         //  cancel
      edit_cancel(0);
      return 1;
   }
   
   if (strmatch(event,"colorsat"))                                               //  saturation slider -1 ... +1
   {
      sat0 = colorsat;                                                           //  old value
      zdialog_fetch(zd,"colorsat",colorsat);                                     //  new value
      dsat = colorsat - sat0;                                                    //  change in saturation, + -

      for (ii = 0; ii < sd->nap[0]; ii++)                                        //  update curve 0 nodes from slider
      {
         dy = sd->apy[0][ii] + 0.5 * dsat;                                       //  increment saturation
         if (dy < 0) dy = 0;
         if (dy > 1) dy = 1;
         sd->apy[0][ii] = dy;
      }

      splcurve_generate(sd,0);                                                   //  regenerate curve 0
      gtk_widget_queue_draw(sd->drawarea);                                       //  draw curve 0
      Fapply = 1;
   }
   
   if (strmatch(event,"paint")) Fapply = 1;                                      //  mouse paint

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

   return 1;
}


//  this function is called when a curve is edited

void saturation_curvedit(int spc)
{
   using namespace saturation_names;
   thread_signal();
   return;
}


//  thread function

void * saturation_thread(void *arg)
{
   using namespace saturation_names;

   void * saturation_wthread(void *);

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

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

   do_wthreads(saturation_wthread,NSMP);

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

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


void * saturation_wthread(void *arg)                                             //  worker thread function
{
   using namespace saturation_names;

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

   while (true)                                                                  //  loop all edit pixels
   {
      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) break;
      if (blend == 0) continue;
      
      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

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

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

      //  apply saturation curve

      if (sd->mod[0])                                                            //  curve was modified
      {
         pixbrite = 0.333 * (R3 + G3 + B3);                                      //  pixel brightness, 0 to 255.9
         ii = coeff * pixbrite;                                                  //  "all" curve index 0-999
         if (ii < 0) ii = 0;
         if (ii > 999) ii = 999;
         colorsat = 2.0 * sd->yval[0][ii] - 1.0;                                 //  -1 ... 0 ... +1

         R9 = R9 + colorsat * (R9 - pixbrite);                                   //  colorsat is -1 ... +1
         G9 = G9 + colorsat * (G9 - pixbrite);
         B9 = B9 + colorsat * (B9 - pixbrite);
      }

      RGBfix(R9,G9,B9);

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

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

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

   return 0;
}


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

//  Soft Focus
//  Mix a sharp and blurred image with blurred portion 0-100%.
//  Blur radius for pixel is 0-value for pixel with 0-255 contrast.


namespace soft_focus_names
{
   editfunc    EFsoft_focus;
   float       Brad, Bmix;
   float       Bweight[101];                                                     //  for max. radius 100
}


//  menu function

void m_soft_focus(GtkWidget *, ch  *menu)
{
   using namespace soft_focus_names;

   int    soft_focus_dialog_event(zdialog* zd, ch  *event);
   void * soft_focus_thread(void *);

   F1_help_topic = "soft focus";

   Plog(1,"m_soft_focus \n");

   EFsoft_focus.menuname = "Soft Focus";
   EFsoft_focus.menufunc = m_soft_focus;
   EFsoft_focus.Farea = 2;                                                       //  select area usable
   EFsoft_focus.Fpaintedits = 1;                                                 //  use with paint edits OK
   EFsoft_focus.Fscript = 1;                                                     //  scripting supported
   EFsoft_focus.threadfunc = soft_focus_thread;

   if (! edit_setup(EFsoft_focus)) return;                                       //  setup edit
   
/***
       ____________________________________________
      |               Soft Focus                   |
      |                                            |
      |  Blur Radius  ========[]=============== 10 |     1-N for pixels with 0-max. contrast
      |  Blur Mix     ================[]======= 65 |     % blurred image - rest from sharp image
      |                                            |
      |                           [Apply] [OK] [X] |
      |____________________________________________|
      
***/

   zdialog *zd = zdialog_new("Soft Focus",Mwin,"Apply","OK"," X ",null);
   EFsoft_focus.zd = zd;

   zdialog_add_widget(zd,"hbox","hbrad","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labrad","hbrad","Blur Radius","space=3");
   zdialog_add_widget(zd,"hscale2","Brad","hbrad","1|30|1|10","expand");
   zdialog_add_widget(zd,"hbox","hbmix","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labmix","hbmix","Blur Mix","space=3");
   zdialog_add_widget(zd,"hscale2","Bmix","hbmix","1|100|1|50","expand");

   zdialog_load_inputs(zd);
   zdialog_resize(zd,350,0);
   zdialog_run(zd,soft_focus_dialog_event,"save");                               //  run dialog - parallel

   return;
}


//  dialog event and completion callback function

int soft_focus_dialog_event(zdialog *zd, ch  *event)
{
   using namespace soft_focus_names;
   
   if (strmatch(event,"focus")) return 1;

   zdialog_fetch(zd,"Brad",Brad);
   zdialog_fetch(zd,"Bmix",Bmix);

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  done
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  cancel
   if (strmatch(event,"apply")) zd->zstat = 1;                                   //  from script
   
   if (zd->zstat == 1) {                                                         //  [apply]
      zd->zstat = 0;                                                             //  keep dialog alive
      edit_reset(); 
      thread_signal();
      return 1;
   }

   if (zd->zstat == 2) {                                                         //  [OK]
      edit_addhist("radius:%.0f mix:%.0f",Brad,Bmix);                            //  edit parms > edit hist
      edit_done(0);
      return 1;
   }

   if (zd->zstat < 0 || zd->zstat > 2) {                                         //  cancel
      edit_cancel(0);
      return 1;
   }
   
   if (strmatch(event,"paint")) thread_signal();                                 //  mouse paint

   return 1;
}


//  thread function

void * soft_focus_thread(void *arg)
{
   using namespace soft_focus_names;

   void * soft_focus_wthread(void *);

   float    ww;

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

   for (int ii = 0; ii <= Brad; ii++)                                            //  set pixel weight per distance
   {                                                                             //      example, Brad = 10
      ww = 1.0 - ii / Brad;                                                      //  dist:   0   1   2   3   5   7   9   10
      ww = ww * ww;                                                              //  weight: 1  .81 .64 .49 .25 .09 .01 .00
      Bweight[ii] = ww;
   }
   
   get_edit_pixels_init(NSMP,0);                                                 //  initz. pixel loop

   do_wthreads(soft_focus_wthread,NSMP);

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

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


void * soft_focus_wthread(void *arg)                                             //  worker thread function
{
   using namespace soft_focus_names;

   int         index = *((int *) arg);
   int         px, py, qx, qy, rx, ry;
   int         Fend, irad;
   float       ww, wsum, blend, bmix1, bmix2;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       *pix1, *pix3, *pixN;
   
   bmix1 = 0.01 * Bmix;                                                          //  0.0 to 1.0
   bmix2 = 1.0 - bmix1;                                                          //  1.0 to 0.0

   while (true)                                                                  //  loop all edit pixels
   {
      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) break;
      if (blend == 0) continue;
      
      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

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

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

      R9 = G9 = B9 = 0;                                                          //  compute blurred pixel
      wsum = 0;
      
      for (qy = -Brad; qy <= +Brad; qy++)                                        //  loop pixels within Brad of (px,py)
      for (qx = -Brad; qx <= +Brad; qx++)
      {
         rx = px + qx;
         ry = py + qy;
         
         if (rx < 0 || rx > Eww-1) continue;                                     //  outside image edge
         if (ry < 0 || ry > Ehh-1) continue;
         
         irad = sqrtf(qx*qx + qy*qy);                                            //  pixel distance from (px,py)
         ww = Bweight[irad];                                                     //  pixel weight for blur

         pixN = PXMpix(E1pxm,rx,ry);                                             //  weighted sum for blur
         R9 += pixN[0] * ww;
         G9 += pixN[1] * ww;
         B9 += pixN[2] * ww;

         wsum += ww; 
      }
      
      if (wsum == 0) continue;
      
      R9 = R9 / wsum;                                                            //  blurred pixel
      G9 = G9 / wsum;
      B9 = B9 / wsum;
      
      R9 = bmix1 * R9 + bmix2 * R1;                                              //  mixed blurred and sharp pixel
      G9 = bmix1 * G9 + bmix2 * G1;
      B9 = bmix1 * B9 + bmix2 * B1;

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

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

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

   return 0;
}


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

//  match_colors 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.

namespace match_colors_names
{
   float    match_colors_RGB1[3];                                                //  image 1 base colors to match
   float    match_colors_RGB2[3];                                                //  image 2 target colors to match
   int      match_colors_radius = 10;                                            //  mouse radius
   int      match_colors_mode = 0;

   editfunc    EFmatchcolors;
}


//  menu function

void m_match_colors(GtkWidget *, ch *menu)
{
   using namespace match_colors_names;

   int    match_colors_dialog_event(zdialog* zd, ch *event);
   void * match_colors_thread(void *);
   void   match_colors_mousefunc();

   ch     *title = "Color Match Images";

   F1_help_topic = "match colors";

   Plog(1,"m_match_colors \n");

/***
          ____________________________________________
         |       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      |
         |                                            |
         |                                   [OK] [X] |
         |____________________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,"OK"," X ",null);                        //  match_colors 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,"zspin","radius","hbrad","1|20|1|10","space=5");
   zdialog_add_widget(zd,"label","labrad","hbrad","mouse radius for color sample");
   zdialog_add_widget(zd,"hbox","hbop1","vb2");
   zdialog_add_widget(zd,"button","open1","hbop1","Open","space=5");
   zdialog_add_widget(zd,"label","labop1","hbop1","image for source color");
   zdialog_add_widget(zd,"hbox","hbclik1","vb2");
   zdialog_add_widget(zd,"label","labclik1","hbclik1","click on image to get source color");
   zdialog_add_widget(zd,"hbox","hbop2","vb2");
   zdialog_add_widget(zd,"button","open2","hbop2","Open","space=5");
   zdialog_add_widget(zd,"label","labop2","hbop2","image to set matching color");
   zdialog_add_widget(zd,"hbox","hbclik2","vb2");
   zdialog_add_widget(zd,"label","labclik2","hbclik2","click on image to set matching color");

   zdialog_stuff(zd,"radius",match_colors_radius);                               //  remember last radius

   EFmatchcolors.menuname = "Match Colors";
   EFmatchcolors.Farea = 2;                                                      //  select area usable
   EFmatchcolors.zd = zd;
   EFmatchcolors.threadfunc = match_colors_thread;
   EFmatchcolors.mousefunc = match_colors_mousefunc;

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

   zdialog_run(zd,match_colors_dialog_event,"parent");                           //  run dialog - parallel
   return;
}


//  match_colors dialog event and completion function

int match_colors_dialog_event(zdialog *zd, ch *event)
{
   using namespace match_colors_names;

   void   match_colors_mousefunc();

   int      err;
   ch       *file;

   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   
   if (zd->zstat)
   {
      if (match_colors_mode == 4) {                                              //  edit was started
         if (zd->zstat == 1) edit_done(0);                                       //  commit edit
         else edit_cancel(0);                                                    //  discard edit
         match_colors_mode = 0;
         return 1;
      }

      freeMouse();                                                               //  abandoned
      zdialog_free(zd);
      match_colors_mode = 0;
      return 1;
   }

   if (strmatch(event,"radius"))                                                 //  set new mouse radius
      zdialog_fetch(zd,"radius",match_colors_radius);

   if (strmatch(event,"open1"))                                                  //  get image 1 for color source
   {
      if (match_colors_mode == 4) edit_cancel(1);                                //  cancel edit, keep dialog
      match_colors_mode = 0;
      file = select_files1(null);                                                //  open image 1
      if (file) {
         err = f_open(file);
         if (! err) match_colors_mode = 1;                                       //  image 1 ready to click
      }
   }

   if (strmatch(event,"open2"))                                                  //  get image 2 to set matching color
   {
      if (match_colors_mode < 2) {
         zmessageACK(Mwin,"select source image color first");                    //  check that RGB1 has been set
         return 1;
      }
      match_colors_mode = 2;
      file = select_files1(null);                                                //  open image 2
      if (! file) return 1;
      err = f_open(file);
      if (err) return 1;
      match_colors_mode = 3;                                                     //  image 2 ready to click
   }

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


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

void match_colors_mousefunc()
{
   using namespace match_colors_names;

   void  match_colors_getRGB(int px, int py, float rgb[3]);

   int      px, py;

   if (match_colors_mode < 1) return;                                            //  no image available yet

   draw_mousecircle(Mxposn,Myposn,match_colors_radius,0,0);                      //  draw circle around pointer

   if (LMclick)
   {
      LMclick = 0;
      px = Mxclick;
      py = Myclick;

      if (match_colors_mode == 1 || match_colors_mode == 2)                      //  image 1 ready to click
      {
         match_colors_getRGB(px,py,match_colors_RGB1);                           //  get RGB1 color
         match_colors_mode = 2;
         return;
      }

      if (match_colors_mode == 3 || match_colors_mode == 4)                      //  image 2 ready to click
      {
         if (match_colors_mode == 4) edit_reset();
         else {
            if (! edit_setup(EFmatchcolors)) return;                             //  setup edit - thread will launch
            match_colors_mode = 4;                                               //  edit waiting for cancel or done
         }

         match_colors_getRGB(px,py,match_colors_RGB2);                           //  get RGB2 color
         thread_signal();                                                        //  update the target image
         return;
      }
   }

   return;
}


//  get the RGB averages for pixels within mouse radius

void match_colors_getRGB(int px, int py, float rgb[3])
{
   using namespace match_colors_names;

   int      radflat1 = match_colors_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_colors_thread(void *)
{
   using namespace match_colors_names;

   void * match_colors_wthread(void *arg);

   do_wthreads(match_colors_wthread,NSMP);                                       //  worker threads
   
   if (sa_stat == sa_stat_fini) sa_postfix();                                    //  if select area, restore unselected

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

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


void * match_colors_wthread(void *arg)                                           //  worker thread function
{
   using namespace match_colors_names;

   int         index = *((int *) (arg));
   int         px, py;
   float       *pix3;
   float       Rred, Rgreen, Rblue;
   float       red, green, blue;

   Rred = match_colors_RGB1[0] / match_colors_RGB2[0];                           //  color adjustment ratios
   Rgreen = match_colors_RGB1[1] / match_colors_RGB2[1];
   Rblue = match_colors_RGB1[2] / match_colors_RGB2[2];

   for (py = index; py < Ehh; py += NSMP)                                        //  loop all image pixels
   for (px = 0; px < Eww; px++)
   {
      pix3 = PXMpix(E3pxm,px,py);

      red = pix3[0] * Rred;                                                      //  adjust colors
      green = pix3[1] * Rgreen;
      blue = pix3[2] * Rblue;

      RGBfix(red,green,blue);

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

   return 0;
}


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

//  add a brightness/color curved ramp to an image, in a chosen direction

namespace brite_ramp_names 
{
   editfunc    EFbrite_ramp;
   int         Fline, linex1, liney1, linex2, liney2;
   float       A, B, C;
   float       ex1, ey1, ex2, ey2;
}


//  menu function

void m_brite_ramp(GtkWidget *, ch  *menu) 
{
   using namespace brite_ramp_names;

   void   brite_ramp_curvedit(int spc);
   int    brite_ramp_dialog_event(zdialog* zd, ch  *event);
   void * brite_ramp_thread(void *);
   void   brite_ramp_mousefunc();

   ch       *mess = "Draw a line across the image in \n"
                    "direction of brightness change.";

   F1_help_topic = "brightness ramp";

   Plog(1,"m_brite_ramp \n");

   EFbrite_ramp.menuname = "Brightness Ramp";
   EFbrite_ramp.menufunc = m_brite_ramp;
   EFbrite_ramp.FprevReq = 1;                                                    //  use preview
   EFbrite_ramp.Fscript = 1;                                                     //  scripting supported
   EFbrite_ramp.Farea = 2;                                                       //  select area usable
   EFbrite_ramp.threadfunc = brite_ramp_thread;
   EFbrite_ramp.mousefunc = brite_ramp_mousefunc;

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

   Fline = 0;                                                                    //  no drawn line initially

/***
          _______________________________________________
         |              Brightness Ramp                  |
         |                                               |
         |    Draw a line across the image in            |
         |    direction of brightness change.            |
         |  ___________________________________________  |
         | |                                           | |                       //  5 curves are maintained:
         | |                                           | |                       //  curve 0: current display curve
         | |                                           | |                       //        1: curve for all colors
         | |         curve edit area                   | |                       //        2,3,4: red, green, blue
         | |                                           | |
         | |                                           | |
         | |                                           | |
         | |___________________________________________| |
         |   (o) all  (o) red  (o) green  (o) blue       |                       //  select curve to display
         |                                               |
         |                              [Reset] [OK] [X] |
         |_______________________________________________|

***/

   zdialog *zd = zdialog_new("Brightness Ramp",Mwin,"Reset","OK"," X ",null);
   EFbrite_ramp.zd = zd;
   zdialog_add_widget(zd,"label","labmess","dialog",mess);
   zdialog_add_widget(zd,"frame","frameH","dialog",0,"expand");                  //  edit-curves
   zdialog_add_widget(zd,"hbox","hbrgb","dialog");                               //  radio buttons all/red/green/blue
   zdialog_add_widget(zd,"radio","all","hbrgb","All","space=5");
   zdialog_add_widget(zd,"radio","red","hbrgb","Red","space=3");
   zdialog_add_widget(zd,"radio","green","hbrgb","Green","space=3");
   zdialog_add_widget(zd,"radio","blue","hbrgb","Blue","space=3");

   GtkWidget *frameH = zdialog_gtkwidget(zd,"frameH");                           //  setup edit curves
   spldat *sd = splcurve_init(frameH,brite_ramp_curvedit);
   EFbrite_ramp.sd = sd;

   sd->Nscale = 1;                                                               //  horizontal fixed line, neutral curve
   sd->xscale[0][0] = 0.01;
   sd->yscale[0][0] = 0.50;
   sd->xscale[1][0] = 0.99;
   sd->yscale[1][0] = 0.50;

   for (int ii = 0; ii < 4; ii++)                                                //  loop curves 0-3
   {
      sd->nap[ii] = 3;                                                           //  initial curves are neutral
      sd->vert[ii] = 0;
      sd->fact[ii] = 0;
      sd->apx[ii][0] = 0.01;
      sd->apx[ii][1] = 0.50;                                                     //  curve 0 = overall brightness
      sd->apx[ii][2] = 0.99;                                                     //  curve 1/2/3 = R/G/B adjustment
      sd->apy[ii][0] = 0.5;
      sd->apy[ii][1] = 0.5;
      sd->apy[ii][2] = 0.5;
      splcurve_generate(sd,ii);
   }

   sd->Nspc = 4;                                                                 //  4 curves
   sd->fact[0] = 1;                                                              //  curve 0 active
   zdialog_stuff(zd,"all",1);                                                    //  stuff default selection, all

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

   takeMouse(brite_ramp_mousefunc,dragcursor);                                   //  connect mouse
   return;
}


//  dialog event and completion callback function

int brite_ramp_dialog_event(zdialog *zd, ch  *event)
{
   using namespace brite_ramp_names;

   void   brite_ramp_mousefunc();

   int      ii, Fupdate = 0;

   spldat *sd = EFbrite_ramp.sd;    

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  apply and quit
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  cancel
   if (strmatch(event,"apply")) Fupdate++;                                       //  from script
   
   if (strmatch(event,"focus")) 
      takeMouse(brite_ramp_mousefunc,dragcursor);                                //  connect mouse
   
   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();
      thread_signal();
      thread_wait();
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  reset
      {
         for (int ii = 0; ii < 4; ii++) {                                        //  loop curves 0-3
            sd->nap[ii] = 3;                                                     //  all curves are neutral
            sd->vert[ii] = 0;
            sd->fact[ii] = 0;
            sd->apx[ii][0] = 0.01;
            sd->apx[ii][1] = 0.50;
            sd->apx[ii][2] = 0.99;
            sd->apy[ii][0] = 0.5;
            sd->apy[ii][1] = 0.5;
            sd->apy[ii][2] = 0.5;
            splcurve_generate(sd,ii);
            sd->fact[ii] = 0;
         }

         sd->fact[0] = 1;
         gtk_widget_queue_draw(sd->drawarea);                                    //  draw curve 0
         zdialog_stuff(zd,"all",1);
         zdialog_stuff(zd,"red",0); 
         zdialog_stuff(zd,"green",0);
         zdialog_stuff(zd,"blue",0);
         edit_reset();                                                           //  restore initial image
         zd->zstat = 0;
         return 1;
      }
         
      if (zd->zstat == 2) {                                                      //  done
         edit_fullsize();                                                        //  get full size image
         thread_signal();
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  discard edit

      Ntoplines = 0;
      Fpaint2();
      return 1;
   }

   if (zstrstr("all red green blue",event))                                      //  new choice of curve
   {
      zdialog_fetch(zd,event,ii);
      if (! ii) return 0;                                                        //  button OFF event, wait for ON event

      for (ii = 0; ii < 4; ii++)
         sd->fact[ii] = 0;
      ii = strmatchV(event,"all","red","green","blue",null);
      ii = ii-1;                                                                 //  new active curve: 0, 1, 2, 3
      sd->fact[ii] = 1;

      splcurve_generate(sd,ii);                                                  //  regenerate curve
      gtk_widget_queue_draw(sd->drawarea);                                       //  draw curve

      Fupdate = 1;
   }

   if (Fupdate) thread_signal();                                                 //  trigger image update

   return 1;
}


//  brite_ramp mouse function

void brite_ramp_mousefunc()
{
   using namespace brite_ramp_names;
   
   int      mx = 0, my = 0;
   float    d1, d2;
   
   if (! (LMclick || RMclick || Mxdrag || Mydrag)) return;                       //  ignore mouse movement
   
   if (LMclick || RMclick) {                                                     //  left or right mouse click
      mx = Mxclick;
      my = Myclick;
      LMclick = RMclick = 0;
   }

   if (Mxdrag || Mydrag) {                                                       //  mouse drag
      mx = Mxdrag;
      my = Mydrag;
      Mxdrag = Mydrag = 0;
   }
   
   if (! Fline && (mx || my))
   {
      Fline = 1;
      linex1 = mx;                                                               //  draw arbitrary line to start with
      liney1 = my;
      linex2 = mx + 100;
      liney2 = my + 100;
   }
   
   else                                                                          //  move nearest line end point to mouse
   {
      d1 = (linex1 - mx) * (linex1 - mx) + (liney1 - my) * (liney1 - my);
      d2 = (linex2 - mx) * (linex2 - mx) + (liney2 - my) * (liney2 - my);

      if (d1 < d2) {
         linex1 = mx;
         liney1 = my;
      }
      else {
         linex2 = mx;
         liney2 = my;
      }
   }
   
   Ntoplines = 1;                                                                //  update line data
   toplines[0].x1 = linex1;
   toplines[0].y1 = liney1;
   toplines[0].x2 = linex2;
   toplines[0].y2 = liney2;
   toplines[0].type = 3;                                                         //  black/white dual line

   thread_signal();                                                              //  update image
   return;
}


//  this function is called when a curve is edited

void brite_ramp_curvedit(int spc)
{
   thread_signal();
   return;
}


//  brite_ramp thread function

void * brite_ramp_thread(void *arg)
{
   using namespace brite_ramp_names;

   void brite_ramp_equation();
   void brite_ramp_rampline();
   void * brite_ramp_wthread(void *);

   ex1 = linex1;                                                                 //  ramp line end points
   ey1 = liney1;
   ex2 = linex2;
   ey2 = liney2;

   brite_ramp_equation();                                                        //  compute line equation
   brite_ramp_rampline();                                                        //  compute new end points

   do_wthreads(brite_ramp_wthread,NSMP);                                         //  worker threads

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

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


void * brite_ramp_wthread(void *arg)                                             //  worker thread function
{
   using namespace brite_ramp_names;

   void brite_ramp_posn(int px, int py, float &rx, float &ry);

   int         index = *((int *) arg);
   int         ii, dist = 0, px3, py3;
   float       x3, y3;
   float       d1, d2, rampval;
   float       *pix1, *pix3;
   float       R1, G1, B1;
   float       R3, G3, B3;
   float       Fall, Fred, Fgreen, Fblue;

   spldat *sd = EFbrite_ramp.sd;    

   for (py3 = index; py3 < Ehh; py3 += NSMP)                                     //  loop output pixels
   for (px3 = 0; px3 < Eww; px3++)
   {
      if (sa_stat == sa_stat_fini) {                                             //  select area active
         ii = py3 * Eww + px3;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel outside area
      }

      brite_ramp_posn(px3,py3,x3,y3);                                            //  nearest point on ramp line

      d1 = sqrtf((x3-ex1) * (x3-ex1) + (y3-ey1) * (y3-ey1));                     //  compute ramp value
      d2 = sqrtf((x3-ex2) * (x3-ex2) + (y3-ey2) * (y3-ey2));
      rampval = d1 / (d1 + d2);                                                  //  0.0 ... 1.0

      ii = 999.0 * rampval;                                                      //  corresp. curve index 0-999

      Fall = sd->yval[0][ii] * 2.0;                                              //  curve values 0.0 - 1.0
      Fred = sd->yval[1][ii] * 2.0;                                              //  (0.5 is neutral value)
      Fgreen = sd->yval[2][ii] * 2.0;
      Fblue = sd->yval[3][ii] * 2.0;
      
      pix1 = PXMpix(E1pxm,px3,py3);                                              //  input pixel
      R1 = pix1[0];
      G1 = pix1[1];
      B1 = pix1[2];
      
      R3 = R1 * Fall;                                                            //  curve "all" adjustment
      G3 = G1 * Fall;                                                            //    projected on each RGB color
      B3 = B1 * Fall;

      R3 = R3 * Fred;                                                            //  add additional RGB adjustments
      G3 = G3 * Fgreen;
      B3 = B3 * Fblue;

      RGBfix(R3,G3,B3);

      pix3 = PXMpix(E3pxm,px3,py3);                                              //  output pixel
      pix3[0] = R3;
      pix3[1] = G3;
      pix3[2] = B3;
   }

   return 0;
}


//  get equation of ramp line in the form  Ax + By + C = 0
//  end points are (ex1,ey1) and (ex2,ey2)

void brite_ramp_equation()
{
   using namespace brite_ramp_names;

   if (ex1 != ex2)
   {
      A = (ey2 - ey1) / (ex2 - ex1);
      B = -1;
      C = ey1 - A * ex1;
   }
   else {
      A = 1;
      B = 0;
      C = -ex1;
   }
   return;
}


//  compute nearest point on ramp line for given image pixel position

void brite_ramp_posn(int px, int py, float &rx, float &ry)
{
   using namespace brite_ramp_names;
   
   float    F1, F2;

   F1 = B * px - A * py;
   F2 = A * A + B * B;
   rx = (B * F1 - A * C) / F2;
   ry = (-A * F1 - B * C) / F2;

   return;
}


//  extend ramp line end points long enough for entire image

void brite_ramp_rampline()
{
   using namespace brite_ramp_names;

   void brite_ramp_posn(int px, int py, float &rx, float &ry);

   float    rx, ry, d1, d2;
   
   if (B == 0) {                                                                 //  vertical line
      ey1 = 0;
      ey2 = Ehh - 1;
      return;
   }

   if (A == 0) {                                                                 //  horizontal line
      ex1 = 0;
      ex2 = Eww - 1;
      return;
   }

   brite_ramp_posn(0,0,rx,ry);
   if (rx < 0 || ry < 0) {
      d1 = (rx - ex1) * (rx - ex1) + (ry - ey1) * (ry - ey1);
      d2 = (rx - ex2) * (rx - ex2) + (ry - ey2) * (ry - ey2);
      if (d1 < d2) {
         ex1 = rx;
         ey1 = ry;
      }
      else {
         ex2 = rx;
         ey2 = ry;
      }
   }      

   brite_ramp_posn(Eww,0,rx,ry);
   if (rx > Eww || ry < 0) {
      d1 = (rx - ex1) * (rx - ex1) + (ry - ey1) * (ry - ey1);
      d2 = (rx - ex2) * (rx - ex2) + (ry - ey2) * (ry - ey2);
      if (d1 < d2) {
         ex1 = rx;
         ey1 = ry;
      }
      else {
         ex2 = rx;
         ey2 = ry;
      }
   }      

   brite_ramp_posn(Eww,Ehh,rx,ry);
   if (rx > Eww || ry > Ehh) {
      d1 = (rx - ex1) * (rx - ex1) + (ry - ey1) * (ry - ey1);
      d2 = (rx - ex2) * (rx - ex2) + (ry - ey2) * (ry - ey2);
      if (d1 < d2) {
         ex1 = rx;
         ey1 = ry;
      }
      else {
         ex2 = rx;
         ey2 = ry;
      }
   }      

   brite_ramp_posn(0,Ehh,rx,ry);
   if (rx < 0 || ry > Ehh) {
      d1 = (rx - ex1) * (rx - ex1) + (ry - ey1) * (ry - ey1);
      d2 = (rx - ex2) * (rx - ex2) + (ry - ey2) * (ry - ey2);
      if (d1 < d2) {
         ex1 = rx;
         ey1 = ry;
      }
      else {
         ex2 = rx;
         ey2 = ry;
      }
   }      

   return;
}


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

   Vignette function

   1. Change the brightness from center to edge using a curve.
   2. Change the color from center to edge using a color and a curve.
      (the pixel varies between original RGB and selected color)
   3. Mouse click or drag on image sets a new vignette center.

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

void  vign_mousefunc();

editfunc    EFvignette;
uint8       vignette_RGB[3] = { 0, 0, 255 };
int         vignette_spc;
float       vign_cx, vign_cy;
float       vign_rad;


void m_vignette(GtkWidget *, ch  *menu)
{
   int      Vign_dialog_event(zdialog *zd, ch  *event);
   void     Vign_curvedit(int);
   void *   Vign_thread(void *);

   ch       *title = "Vignette";

   F1_help_topic = "vignette";

   Plog(1,"m_vignette \n");

   EFvignette.menuname = "Vignette";
   EFvignette.Farea = 2;                                                         //  select area usable
   EFvignette.FprevReq = 1;                                                      //  use preview image
   EFvignette.threadfunc = Vign_thread;                                          //  thread function
   EFvignette.mousefunc = vign_mousefunc;                                        //  mouse function

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

/***
          ___________________________________
         |  _______________________________  |
         | |                               | |
         | |                               | |
         | |    curve drawing area         | |
         | |                               | |
         | |                               | |
         | |_______________________________| |
         |  center                     edge  |
         |                                   |
         |  (o) Brightness  (o) Color [___]  |
         |                                   |
         |                          [OK] [X] |
         |___________________________________|

***/

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

   zdialog_add_widget(zd,"frame","frame","dialog",0,"expand");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labcenter","hb1","Center","space=4");
   zdialog_add_widget(zd,"label","space","hb1",0,"expand");
   zdialog_add_widget(zd,"label","labedge","hb1","Edge","space=5");

   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
   zdialog_add_widget(zd,"radio","RBbrite","hb2","Brightness","space=5");
   zdialog_add_widget(zd,"radio","RBcolor","hb2","Color","space=5");
   zdialog_add_widget(zd,"colorbutt","color","hb2","0|0|255");

   vignette_RGB[0] = vignette_RGB[1] = 0;                                        //  initial color = blue
   vignette_RGB[2] = 255;

   vign_cx = Eww / 2;                                                            //  initial vignette center
   vign_cy = Ehh / 2;

   vign_rad = vign_cx * vign_cx + vign_cy * vign_cy;                             //  radius = distance to corners
   vign_rad = sqrtf(vign_rad);

   zdialog_stuff(zd,"RBbrite",1);                                                //  default curve = brightness

   GtkWidget *frame = zdialog_gtkwidget(zd,"frame");                             //  set up curve edit
   spldat *sd = splcurve_init(frame,Vign_curvedit);
   EFvignette.sd = sd;

   sd->Nspc = 2;                                                                 //  2 curves

   sd->vert[0] = 0;                                                              //  curve 0 = brightness curve
   sd->nap[0] = 2;
   sd->apx[0][0] = 0.01;
   sd->apy[0][0] = 0.5;
   sd->apx[0][1] = 0.99;
   sd->apy[0][1] = 0.5;
   splcurve_generate(sd,0);

   sd->vert[1] = 0;                                                              //  curve 1 = color curve
   sd->nap[1] = 2;
   sd->apx[1][0] = 0.01;
   sd->apy[1][0] = 0.01;
   sd->apx[1][1] = 0.99;
   sd->apy[1][1] = 0.01;
   splcurve_generate(sd,1);

   vignette_spc = 0;                                                             //  initial curve = brightness
   sd->fact[0] = 1;
   sd->fact[1] = 0; 

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

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


//  dialog event and completion callback function

int Vign_dialog_event(zdialog *zd, ch  *event)
{
   void     Vign_curvedit(int);

   spldat   *sd = EFvignette.sd;    
   int      ii;
   ch       color[20];
   ch       *ppc;

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

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  done
         thread_wait();                                                          //  insure thread done
         float R = 1.0 * E0pxm->ww / Eww;                                        //  bugfix                                24.40
         vign_cx = R * vign_cx;                                                  //  scale geometries to full size
         vign_cy = R * vign_cy;
         vign_rad = R * vign_rad;
         edit_fullsize();                                                        //  get full size image
         thread_signal();
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

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

   if (strmatchN(event,"RB",2)) {                                                //  new choice of curve
      sd->fact[0] = sd->fact[1] = 0; 
      ii = strmatchV(event,"RBbrite","RBcolor",null);
      vignette_spc = ii = ii - 1;
      sd->fact[ii] = 1;                                                          //  active curve
      splcurve_generate(sd,ii);                                                  //  regenerate curve
      gtk_widget_queue_draw(sd->drawarea);                                       //  draw curve
      thread_signal();
   }

   if (strmatch(event,"color")) {                                                //  change color
      zdialog_fetch(zd,"color",color,19);                                        //  get color from color wheel
      ppc = substring(color,"|",1);
      if (ppc) vignette_RGB[0] = atoi(ppc);
      ppc = substring(color,"|",2);
      if (ppc) vignette_RGB[1] = atoi(ppc);
      ppc = substring(color,"|",3);
      if (ppc) vignette_RGB[2] = atoi(ppc);
      thread_signal();                                                           //  trigger update thread
   }

   return 1;
}


//  get mouse position and set new center for vignette

void vign_mousefunc()                                                            //  mouse function
{
   if (! LMclick && ! Mdrag) return;
   LMclick = 0;

   vign_cx = Mxposn;                                                             //  new vignette center = mouse position
   vign_cy = Myposn;

   Mxdrag = Mydrag = 0;

   thread_signal();                                                              //  trigger image update
   return;
}


//  this function is called when the curve is edited

void Vign_curvedit(int)
{
   thread_signal();                                                              //  update image
   return;
}


//  thread function

void * Vign_thread(void *)
{
   void * Vign_wthread(void *arg);

   do_wthreads(Vign_wthread,NSMP);                                               //  worker threads

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

   Fpaint2();
   return 0;
}


//  working thread

void * Vign_wthread(void *arg)
{
   float       *pix1, *pix3;
   int         index, ii, kk, px, py, dist = 0;
   float       cx, cy, rad, radx, rady, f1, f2, xval, yval;
   float       R1, G1, B1, R3, G3, B3;
   spldat      *sd = EFvignette.sd;

   cx = vign_cx;                                                                 //  vignette center (mouse)
   cy = vign_cy;

   index = *((int *) arg);

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

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

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

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

      radx = px - cx;                                                            //  distance from vignette center
      rady = py - cy;
      rad = sqrtf(radx*radx + rady*rady);                                        //  (px,py) distance from center

      xval = rad / vign_rad;                                                     //  scale 0 to 1.0
      kk = 999.0 * xval;                                                         //  scale 0 to 999
      if (kk > 999) kk = 999;                                                    //  beyond radius

      yval = sd->yval[0][kk];                                                    //  brightness curve y-value 0 to 1.0
      if (yval > 1.0) yval = 1.0;
      yval = 2.0 * yval;                                                         //  0 to 2.0

      R3 = yval * R1;                                                            //  adjust brightness
      G3 = yval * G1;
      B3 = yval * B1;

      yval = sd->yval[1][kk];                                                    //  color curve y-value 0 to 1.0
      if (yval > 1.0) yval = 1.0;
      f1 = yval;                                                                 //  0 to 1.0   new color
      f2 = 1.0 - f1;                                                             //  1.0 to 0   old color

      R3 = f1 * vignette_RGB[0] + f2 * R3;                                       //  mix input and vignette color
      G3 = f1 * vignette_RGB[1] + f2 * G3;
      B3 = f1 * vignette_RGB[2] + f2 * B3;

      RGBfix(R3,G3,B3);
      
      pix3[0] = R3;
      pix3[1] = G3;
      pix3[2] = B3;
   }

   return 0;
}



