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

   Fotocx - edit photos and manage collections

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

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

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

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

   Fotocx image edit - Refine menu functions

   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)

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

//  Flatten brightness histogram.
//  The new brightness for each pixel is based on the brightness histogram             //  26.0
//    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]
   float    F1, F2, F3, F4, F5;              //  zone slider values, 0 - 1.0
   float    FA, PFA;                         //  "all" zone and prior value
   float    Fpix[256];                       //  pixel flatten adjustment factor

   editfunc    EFflathist;
}


//  menu function

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

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

   ch    *title = TX("Flatten Histogram");

   F1_help_topic = TX("FLATTEN HISTOGRAM");

   printf("m_flat_hist \n");

   if (! curr_file) {
      zmessageACK(Mwin,TX("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;
   printf("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]        |
         |                                |
         | zone 1 =============[]======== |
         | zone 2 =========[]============ |
         | zone 3 ======[]=============== |
         | zone 4 ===[]================== |
         | zone 5 []===================== |
         | zone A []===================== |
         |                                |
         |               [Reset] [OK] [X] |
         |________________________________|

***/

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

   zdialog_add_widget(zd,"hbox","hbrad","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labrad","hbrad",TX("Radius"),"space=3");
   zdialog_add_widget(zd,"zspin","radius","hbrad","20|999|1|50");
   zdialog_add_widget(zd,"button","apply","hbrad",TX("Apply"),"space=10");
   zdialog_add_widget(zd,"hbox","sep1","dialog",0,"space=3");

   zdialog_add_widget(zd,"hbox","hbz1","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labz1","hbz1","zone 1","space=3");
   zdialog_add_widget(zd,"hscale","zone1","hbz1","0|1.0|0.001|0","expand|space=3");

   zdialog_add_widget(zd,"hbox","hbz2","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labz2","hbz2","zone 2","space=3");
   zdialog_add_widget(zd,"hscale","zone2","hbz2","0|1.0|0.001|0","expand|space=3");

   zdialog_add_widget(zd,"hbox","hbz3","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labz3","hbz3","zone 3","space=3");
   zdialog_add_widget(zd,"hscale","zone3","hbz3","0|1.0|0.001|0","expand|space=3");

   zdialog_add_widget(zd,"hbox","hbz4","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labz4","hbz4","zone 4","space=3");
   zdialog_add_widget(zd,"hscale","zone4","hbz4","0|1.0|0.001|0","expand|space=3");

   zdialog_add_widget(zd,"hbox","hbz5","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labz5","hbz5","zone 5","space=3");
   zdialog_add_widget(zd,"hscale","zone5","hbz5","0|1.0|0.001|0","expand|space=3");

   zdialog_add_widget(zd,"hbox","hbzA","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labzA","hbzA","zone A","space=3");
   zdialog_add_widget(zd,"hscale","zoneA","hbzA","0|1.0|0.001|0","expand|space=3");
   
   zdialog_rescale(zd,"zone1",0,0,1);                                                  //  expand scale at low end               26.4
   zdialog_rescale(zd,"zone2",0,0,1);
   zdialog_rescale(zd,"zone3",0,0,1);
   zdialog_rescale(zd,"zone4",0,0,1);
   zdialog_rescale(zd,"zone5",0,0,1);
   zdialog_rescale(zd,"zoneA",0,0,1);

   zrad = 50;                                                                          //  initial zone radius (working image)
   radius = zrad * Rscale;                                                             //  corresp. full size image radius
   zdialog_stuff(zd,"radius",radius);

   zdialog_run(zd,flathist_dialog_event,"save");                                       //  start dialog

   flathist_calczones();                                                               //  calculate zone histograms
   
   F1 = F2 = F3 = F4 = F5 = FA = PFA = 0;                                              //  initial slider values

   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      ii, Fdoflat = 0;
   float    G1, G2, G3, G4, G5, G6;

   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;
      zadd_locked(NFbusy,+1);
      flathist_calczones();                                                            //  calculate new zone histograms
      zadd_locked(NFbusy,-1);
      Fdoflat = 1;
   }

   if (strmatch(event,"zoneA"))                                                        //  move all sliders by zoneA slider
   {
      zdialog_fetch(zd,"zoneA",FA);
      F1 += FA - PFA;
      F2 += FA - PFA;
      F3 += FA - PFA;
      F4 += FA - PFA;
      F5 += FA - PFA;
      PFA = FA;
      zdialog_stuff(zd,"zone1",F1);
      zdialog_stuff(zd,"zone2",F2);
      zdialog_stuff(zd,"zone3",F3);
      zdialog_stuff(zd,"zone4",F4);
      zdialog_stuff(zd,"zone5",F5);
   }      

   if (strstr("zone1 zone2 zone3 zone4 zone5 zoneA",event)) 
   {
      zdialog_fetch(zd,"zone1",F1);                                                    //  zone slider values, 0 - 1.0
      zdialog_fetch(zd,"zone2",F2);
      zdialog_fetch(zd,"zone3",F3);
      zdialog_fetch(zd,"zone4",F4);
      zdialog_fetch(zd,"zone5",F5);

      for (ii = 0; ii < 256; ii++)                                                     //  calculate brightness adjustment
      {                                                                                //    factors for RGB values 0-255
         G1 = G2 = G3 = G4 = G5 = G6 = 0;

         if (ii < 51)       { G1 = ( 51-ii) / 51.0;  G2 = (ii - 0  ) / 51.0; }         //  darkest RGB in zones 1 and 2
         else if (ii < 102) { G2 = (102-ii) / 51.0;  G3 = (ii - 51 ) / 51.0; }
         else if (ii < 153) { G3 = (153-ii) / 51.0;  G4 = (ii - 102) / 51.0; }
         else if (ii < 204) { G4 = (204-ii) / 51.0;  G5 = (ii - 153) / 51.0; }
         else if (ii < 256) { G5 = (256-ii) / 51.0;  G6 = (ii - 204) / 51.0; }         //  brightest in zones 5 and 6

         Fpix[ii] = F1 * (G1 + G2) + F2 * (G2 + G3) + F3 * (G3 + G4)                   //  apply zone slider values
                  + F4 * (G4 + G5) + F5 * (G5 + G6);
         Fpix[ii] *= 0.5;                                                              //  dampen
      }
      
      Fdoflat = 1;
   }
   
   if (Fdoflat)
   {
      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,"zone1",0);
         zdialog_stuff(zd,"zone2",0);
         zdialog_stuff(zd,"zone3",0);
         zdialog_stuff(zd,"zone4",0);
         zdialog_stuff(zd,"zone5",0);
         zdialog_stuff(zd,"zoneA",0);
         F1 = F2 = F3 = F4 = F5 = FA = 0;
         return 1;
      }

      if (zd->zstat == 2)                                                              //  [OK]
      {
         PXB_free(Rpxb);
         zfree(Bhist);
         edit_addhist("Rad:%d zones: %.2f %.2f %.2f %.2f %.2f",
                                          radius,F1,F2,F3,F4,F5);
         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;
   float    *pix1, *pix3;

   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];                                                                    //  curr. output RGB
      G3 = pix3[1];
      B3 = pix3[2];

      B = 0.333 * (R1 + G1 + B1);                                                      //  input pixel brightness 0-255
      if (B < 0 || B > 255) continue;                                                  //  ignore invalid pixel

      zone = py / Rscale * Rww + px / Rscale;                                          //  input pixel zone
      ii = 256 * zone + B;
      BF = Bhist[ii];                                                                  //  flatten brightness factor  0 .. N

      R9 = R1 * BF;                                                                    //  new output RGB
      G9 = G1 * BF;                                                                    //  (if all zone sliders = 1.0)
      B9 = B1 * BF;
      
      ii = int(B);
      R9 = R9 * Fpix[ii] + R1 * (1.0 - Fpix[ii]);                                      //  apply brightness zone factors
      G9 = G9 * Fpix[ii] + G1 * (1.0 - Fpix[ii]);
      B9 = B9 * Fpix[ii] + B1 * (1.0 - Fpix[ii]);

      RGBfix(R9,G9,B9);                                                                //  insure RGB values within 0-255

      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         Zrad;                                                                   //  zone radius
   float       *Zbrite;                                                                //  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)                                                //  revised                               26.0
{
   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

   F1_help_topic = TX("LOCAL CONTRAST");

   printf("m_localcon \n");

   ch  *title = TX("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);
   Zbrite = (float *) zmalloc(cc,"localcon");                                          //  memory for zone brightness

/***
          ________________________________
         |        Local Contrast          |
         |  ____________________________  |
         | |                            | |
         | |                            | |
         | | -------------------------- | |                                            //  curve edit area
         | |                            | |
         | |____________________________| |
         | |____________________________| |                                            //  brightness scale: black to white
         |                                |
         | Power   =============[]======  |
         | Radius  ========[]===========  |
         | Color  ==========[]==========  |
         |                                |
         |               [Reset] [OK] [X] |
         |________________________________|

***/

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

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

   zdialog_add_widget(zd,"hbox","hbpow","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labpow","hbpow",TX("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",TX("Radius"),"space=5");
   zdialog_add_widget(zd,"hscale2","Zrad","hbrad","20|80|1|20","expand");              //  Zrad range 20-100 pixels
   zdialog_add_widget(zd,"hbox","hbcolor","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labcolor","hbcolor",TX("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
   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

   Zrad = 20;                                                                          //  initial values
   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
      event = "power";
      zd->zstat = 2;
   }

   if (strmatch(event,"Zrad")) {                                                       //  zone radius input
      zdialog_fetch(zd,"Zrad",Zrad);
      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,"Zrad",20);
         Zrad = 20;                                                                    //  initial values
         Fnewrad = 1;
         Fpower = 0;
         Fnewpower = 0;
         Fcolor = 0;
         Fnewcolor = 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 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,Zrad,Fcolor);                //  edit parms > edit hist
         edit_done(0);
      }

      else edit_cancel(0);                                                             //  discard edit

      zfree(Zbrite);                                                                   //  free memory
      return 1;
   }

   return 1;
}


//  this function is called when the curve is edited

void localcon_curvedit(int spc)
{
   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_zrad(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);                                                     //  initz. progress counter
      Fnewrad = 0;
      get_edit_pixels_init(NSMP,0);                                                    //  compute inner zone brightness
      do_wthreads(localcon_wthread_zrad,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.
//  Zbrite[*]: brightness for zone around pixel [*]

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

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

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

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

      if (px1 == 0) {
         sum = count = 0;
         for (py2 = pylo; py2 < pyhi; py2++)
         for (px2 = pxlo; px2 < pxhi; px2++) {
            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++) {
            if (sa_stat == sa_stat_fini) {
               ii = Eww * py2 + px3;
               if (sa_pixmap[ii] < 1) continue;
            }
            pix = PXMpix(E1pxm,px3,py2);
            sum -= 0.001302 * (pix[0] + pix[1] + pix[2]);
            count--;
         }
      }

      if (pxhi < Eww) {
         px3 = pxhi;
         for (py2 = pylo; py2 < pyhi; py2++) {
            if (sa_stat == sa_stat_fini) {
               ii = Eww * py2 + px3;
               if (sa_pixmap[ii] < 1) continue;
            }
            pix = PXMpix(E1pxm,px3,py2);
            sum += 0.001302 * (pix[0] + pix[1] + pix[2]);
            count++;
         }
      }

      ii = py1 * Eww + px1;
      if (count > 0) Zbrite[ii] = sum / count;
      else Zbrite[ii] = 0;

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

   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, BR1, BR2, BR3, FC1, FC2;
   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;                                                           //  black

      ii = py1 * Eww + px1;
      Bz1 = Zbrite[ii];                                                                //  zone mean brightness, 0-1
      if (Bz1 == 0) continue;                                                          //  black

      BR1 = Bp / Bz1;                                                                  //  pixel/zone brightness ratio
      BR2 = BR1 * Bp;                                                                  //  new pixel brightness
      
      ii = 500 * BR2 + 500 * Bz1;                                                      //  pixel + zone brightness 0-999
      if (ii < 0) ii = 0;
      if (ii > 999) ii = 999;

      FC1 = sd->yval[0][ii];                                                           //  brightness curve 0 - 0.5 - 1.0
      FC2 = FC1 * 2;                                                                   //                   0 - 1.0 - 2.0
      BR3 = BR2 * FC2;                                                                 //  new pixel brightness

      R9 = BR3/Bp * R1;                                                                //  modified RGB values
      G9 = BR3/Bp * G1;
      B9 = BR3/Bp * 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);
      }

      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;
      }
      
      RGBfix(R3,G3,B3);                                                                //  26.0

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

   F1_help_topic = TX("AMPLIFY CONTRAST");

   printf("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",TX("low"),"space=4");
   zdialog_add_widget(zd,"label","labcM","hb1",TX("Contrast"),"expand");
   zdialog_add_widget(zd,"label","labcH","hb1",TX("high"),"space=5");

   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labcon","hb2",TX("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 = TX("GLOBAL RETINEX");

   printf("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
         |  Multiplier:  [__]  [__]  [__]  [__] |        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(TX("Global Retinex"),Mwin,TX("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",TX("Dark Point"));
   zdialog_add_widget(zd,"label","labbrite","vb1",TX("Bright Point"));
   zdialog_add_widget(zd,"label","labmpy","vb1",TX("Multiplier"));

   zdialog_add_widget(zd,"label","labred","vb2",TX("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",TX("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",TX("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",TX("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",TX("brightness rescale"),"space=5");

   zdialog_add_widget(zd,"hbox","hbclicks","dialog");
   zdialog_add_widget(zd,"check","brightpoint","hbclicks",TX("click bright point"),"space=3");
   zdialog_add_widget(zd,"check","darkpoint","hbclicks",TX("click dark point"),"space=5");

   zdialog_add_widget(zd,"hbox","hbb","dialog");
   zdialog_add_widget(zd,"label","labb","hbb",TX("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",TX("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;
   }

   printf("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 = TX("LOCAL RETINEX");

   printf("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(TX("Local Retinex"),Mwin,TX("Reset"),"OK","X",null);
   EFlretinex.zd = zd;

   zdialog_add_widget(zd,"hbox","hbr","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labr","hbr",TX("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",TX("apply"),"space=3");
   zdialog_add_widget(zd,"hbox","hbb","dialog");
   zdialog_add_widget(zd,"label","labb","hbb",TX("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",TX("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",TX("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
   {
      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,TX("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 = TX("SATURATION");

   printf("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(TX("Saturation"),Mwin,TX("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",TX("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 = TX("SOFT FOCUS");

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

***/

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

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

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

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

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

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

   if (strstr("Brad Bmix",event)) {                                                    //  interactive                           26.2
      zdialog_fetch(zd,"Brad",Brad);
      zdialog_fetch(zd,"Bmix",Bmix);
      thread_signal();
   }

   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 = TX("Color Match Images");

   F1_help_topic = TX("MATCH COLORS");

   printf("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",TX("mouse radius for color sample"));
   zdialog_add_widget(zd,"hbox","hbop1","vb2");
   zdialog_add_widget(zd,"button","open1","hbop1",TX("Open"),"space=5");
   zdialog_add_widget(zd,"label","labop1","hbop1",TX("image for source color"));
   zdialog_add_widget(zd,"hbox","hbclik1","vb2");
   zdialog_add_widget(zd,"label","labclik1","hbclik1",TX("click on image to get source color"));
   zdialog_add_widget(zd,"hbox","hbop2","vb2");
   zdialog_add_widget(zd,"button","open2","hbop2",TX("Open"),"space=5");
   zdialog_add_widget(zd,"label","labop2","hbop2",TX("image to set matching color"));
   zdialog_add_widget(zd,"hbox","hbclik2","vb2");
   zdialog_add_widget(zd,"label","labclik2","hbclik2",TX("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,TX("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 = TX("Draw a line across the image in \n"
                       "direction of brightness change.");

   F1_help_topic = TX("BRIGHTNESS RAMP");

   printf("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(TX("Brightness Ramp"),Mwin,TX("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",TX("All"),"space=5");
   zdialog_add_widget(zd,"radio","red","hbrgb",TX("Red"),"space=3");
   zdialog_add_widget(zd,"radio","green","hbrgb",TX("Green"),"space=3");
   zdialog_add_widget(zd,"radio","blue","hbrgb",TX("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 = TX("VIGNETTE");

   printf("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",TX("Center"),"space=4");
   zdialog_add_widget(zd,"label","space","hb1",0,"expand");
   zdialog_add_widget(zd,"label","labedge","hb1",TX("Edge"),"space=5");

   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
   zdialog_add_widget(zd,"radio","RBbrite","hb2",TX("Brightness"),"space=5");
   zdialog_add_widget(zd,"radio","RBcolor","hb2",TX("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
         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;
}



