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

   Fotoxx      edit photos and manage collections

   Copyright 2007-2020 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.

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

   Fotoxx image edit - Effects menu functions - paint, sketch ... and 'filters'

   m_sketch                convert photo to simulated sketch
   m_cartoon               convert image to cartoon drawing
   m_line_drawing          combine image with highlighted edge pixels
   m_emboss                convert image to simulated embossing (3D relief)
   m_tiles                 convert image to square tiles with 3D edges
   m_dither                parent menu for four dither functions
   m_dither0               Roy Lichtenstein effect
   m_dither1               pure RGB or black/white dots
   m_dither2               RGB mix with given bit-depth
   m_dither3               custom palette colors 
   m_painting              convert image to simulated painting
   m_texture               apply a random texture to an image
   m_pattern               tile an image with a repeating pattern
   m_mosaic                convert image to mosaic of thumbnail images
   m_color_mode            convert between color, B&W, sepia, positive, negative
   m_color_depth           reduce color depth from 16 ... 1 bits
   m_shift_colors          gradually shift selected RGB colors into other colors
   m_alien_colors          change color hues with an algorithm
   m_brite_ramp            adjust brightness graduaally across the image
   m_paint_transp          paint more or less transparency on an image
   m_mirror                horizontal or vertical mirror image
   m_anykernel             transform an image using any custom kernel

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

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

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

//  sketch menu function - convert photo to simulated sketch

namespace sketch_names
{
   editfunc    EFsketch;                                                         //  edit function data
   float       Bweight = 0.5;                                                    //  brightness weight 0-1
   int         Brthresh = 255;                                                   //  brightness threshold 0-255
   float       Cweight = 0.5;                                                    //  contrast weight 0-1
   int         cliplev = 255;                                                    //  clipping level 0-255
   int         algorithm = 1;                                                    //  which algorithm to use
   uint8       fgrgb[3] = { 0, 0, 0 };                                           //  foreground color (black)
   uint8       bgrgb[3] = { 255, 255, 255 };                                     //  background color (white)
   int         ww, hh, cc;                                                       //  image dimensions
   uint8       *britness;                                                        //  maps pixel brightness 0-255
   uint8       *CLcont;                                                          //  maps pixel color contrast 0-255
   uint8       *monopix;                                                         //  output pixel map 0-255
   uint8       *fclip;                                                           //  output clipping flag 0/1
}


void m_sketch(GtkWidget *, const char *menu)
{
   using namespace sketch_names;

   int    sketch_dialog_event(zdialog* zd, const char *event);
   void * sketch_thread(void *);

   int      ii, px, py;
   float    *pix0, *pix1, *pix2, *pix3, *pix4;
   float    f1, f2;
   cchar    *title = E2X("Convert to Sketch");

   F1_help_topic = "sketch";

   EFsketch.menuname = menu;
   EFsketch.menufunc = m_sketch;
   EFsketch.funcname = "sketch";                                                 //  function name
   EFsketch.Farea = 2;                                                           //  select area usable
   EFsketch.Fscript = 1;                                                         //  scripting supported
   EFsketch.threadfunc = sketch_thread;                                          //  thread function
   if (! edit_setup(EFsketch)) return;                                           //  setup edit

   ww = E3pxm->ww;
   hh = E3pxm->hh;
   cc = ww * hh;

   britness = (uint8 *) zmalloc(cc);
   CLcont = (uint8 *) zmalloc(cc);
   monopix = (uint8 *) zmalloc(cc);
   fclip = (uint8 *) zmalloc(cc);

   for (py = 1; py < hh-1; py++)                                                 //  precalculate pixel brightness
   for (px = 1; px < ww-1; px++)
   {
      pix0 = PXMpix(E1pxm,px,py);
      ii = py * ww + px;
      britness[ii] = 0.333 * (pix0[0] + pix0[1] + pix0[2]);                      //  brightness 0-255  (pixbright() nodiff)
   }

   for (py = 1; py < hh-1; py++)                                                 //  precalculate pixel contrast
   for (px = 1; px < ww-1; px++)
   {
      pix0 = PXMpix(E1pxm,px,py);

      pix1 = PXMpix(E1pxm,px-1,py);                                              //  get pixel contrast
      pix2 = PXMpix(E1pxm,px+1,py);
      pix3 = PXMpix(E1pxm,px,py-1);
      pix4 = PXMpix(E1pxm,px,py+1);

      f1 = PIXMATCH(pix1,pix2);                                                  //  0..1 = zero..perfect match
      f2 = PIXMATCH(pix3,pix4);

      ii = py * ww + px;
      CLcont[ii] = 255.0 * (1.0 - f1 * f2);                                      //  pixel color contrast 0-255
   }

/***
        _________________________________________
       |        Convert to Sketch                |
       |                                         |
       |  Brightness ==============[]==========  |
       |  Threshold ======[]===================  |
       |  Contrast =========[]=================  |
       |  Clip Level ===================[]=====  |
       |  Algorithm  (o) #1  (o) #2              |
       |  Foreground [#####]  Background [#####] |
       |                                         |
       |                        [done] [cancel]  |
       |_________________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,Bdone,Bcancel,null);                     //  sketch dialog
   EFsketch.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labbrite","hb1",Bbrightness,"space=5");
   zdialog_add_widget(zd,"hscale","Bweight","hb1","0.0|1.0|0.005|0.5","expand|space=3");

   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","laBrthresh","hb2",Bthresh,"space=5");
   zdialog_add_widget(zd,"hscale","Brthresh","hb2","0|255|1|255","expand|space=3");

   zdialog_add_widget(zd,"hbox","hb3","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labcon","hb3",Bcontrast,"space=5");
   zdialog_add_widget(zd,"hscale","Cweight","hb3","0.0|1.0|0.005|0.5","expand|space=3");

   zdialog_add_widget(zd,"hbox","hb4","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labclip","hb4",E2X("Clip Level"),"space=5");
   zdialog_add_widget(zd,"hscale","cliplev","hb4","0|255|1|0","expand|space=3");

   zdialog_add_widget(zd,"hbox","hb5","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labalg","hb5",E2X("Algorithm"),"space=5");
   zdialog_add_widget(zd,"radio","algorithm1","hb5","#1","space=5");
   zdialog_add_widget(zd,"radio","algorithm2","hb5","#2","space=5");

   zdialog_add_widget(zd,"hbox","hb6","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labfg","hb6",E2X("Foreground"),"space=2");
   zdialog_add_widget(zd,"colorbutt","fgcolor","hb6","0|0|0","space=2");
   zdialog_add_widget(zd,"label","space","hb6",0,"space=8");
   zdialog_add_widget(zd,"label","labbg","hb6",E2X("Background"),"space=2");
   zdialog_add_widget(zd,"colorbutt","bgcolor","hb6","255|255|255","space=2");

   Bweight = 0.5;                                                                //  initial values
   Brthresh = 255;
   Cweight = 0.5;
   cliplev = 255;
   algorithm = 1;
   zdialog_stuff(zd,"algorithm1",1);

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

   signal_thread();
   return;
}


//  sketch dialog event and completion function

int sketch_dialog_event(zdialog *zd, const char *event)
{
   using namespace sketch_names;

   int      ii;
   cchar    *pp;
   char     color[20];
   
   if (strmatch(event,"focus")) return 1;
   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()

   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      zfree(britness);
      zfree(CLcont);
      zfree(monopix);
      zfree(fclip);
      return 1;
   }

   zdialog_fetch(zd,"Bweight",Bweight);                                          //  revised brightness weight
   zdialog_fetch(zd,"Brthresh",Brthresh);                                        //  brightness threshold
   zdialog_fetch(zd,"Cweight",Cweight);                                          //  contrast weight

   zdialog_fetch(zd,"cliplev",cliplev);
   cliplev = 255 - cliplev;                                                      //  scale is reversed

   zdialog_fetch(zd,"algorithm1",ii);
   if (ii == 1) algorithm = 1;
   else algorithm = 2;

   zdialog_fetch(zd,"fgcolor",color,19);
   pp = strField(color,"|",1);
   if (pp) fgrgb[0] = atoi(pp);
   pp = strField(color,"|",2);
   if (pp) fgrgb[1] = atoi(pp);
   pp = strField(color,"|",3);
   if (pp) fgrgb[2] = atoi(pp);

   zdialog_fetch(zd,"bgcolor",color,19);
   pp = strField(color,"|",1);
   if (pp) bgrgb[0] = atoi(pp);
   pp = strField(color,"|",2);
   if (pp) bgrgb[1] = atoi(pp);
   pp = strField(color,"|",3);
   if (pp) bgrgb[2] = atoi(pp);

   signal_thread();

   return 1;
}


//  thread function - multiple working threads to update image

void * sketch_thread(void *)
{
   using namespace sketch_names;

   void sketch_algorithm1();
   void sketch_algorithm2();
   void sketch_finish();

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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      if (algorithm == 1) sketch_algorithm1();
      if (algorithm == 2) sketch_algorithm2();
      sketch_finish();

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

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop warning
}


//  algorithm 1

void sketch_algorithm1()
{
   using namespace sketch_names;

   int         px, py, qx, qy;
   int         ii, jj, kk, dist;
   int         br0, br1, br2, br3, move;
   float       bright, cont;

   for (py = 0; py < hh; py++)                                                   //  convert to monocolor image
   for (px = 0; px < ww; px++)                                                   //    with brightness range 0-255
   {
      ii = py * ww + px;

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

      bright = britness[ii];
      if (bright > Brthresh) bright = 255;
      cont = CLcont[ii];                                                         //  contrast 0-255
      bright = Bweight * bright - Cweight * cont;
      if (bright < 0) bright = 0;
      monopix[ii] = bright;
   }

   for (br0 = 5; br0 <= 255; br0 += 10)                                          //  brightness threshold, 5 ... 255
   {
      for (py = 1; py < hh-1; py++)                                              //  loop all image pixels
      for (px = 1; px < ww-1; px++)                                              //  (interior pixels only)
      {
         ii = py * ww + px;

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

         br1 = monopix[ii];                                                      //  pixel brightness
         if (br1 == 0) continue;                                                 //  skip black pixel
         if (br1 > br0) continue;                                                //  skip pixel brighter than br0

         for (qy = py-1; qy <= py+1; qy++)                                       //  loop pixels within 1 of target pixel
         for (qx = px-1; qx <= px+1; qx++)                                       //  test if target pixel is darkest > 0
         {
            jj = qy * ww + qx;
            br2 = monopix[jj];
            if (br2 == 0) continue;                                              //  ignore black pixels
            if (br2 < br1) goto nextpixel;                                       //  target pixel not darkest
         }

         br2 = 0;
         kk = -1;

         for (qy = py-1; qy <= py+1; qy++)                                       //  loop pixels within 1 of target pixel
         for (qx = px-1; qx <= px+1; qx++)                                       //  find brightest pixel < 255
         {
            jj = qy * ww + qx;
            br3 = monopix[jj];
            if (br3 < 255 && br3 > br2) {
               br2 = br3;
               kk = jj;
            }
         }

         if (kk < 0) continue;

         move = br1;                                                             //  move brightness from target pixel,
         if (255 - br2 < move) move = 255 - br2;                                 //    as much as possible
         monopix[ii] -= move;
         monopix[kk] += move;

         nextpixel: continue;
      }

      zmainloop();
   }

   return;                                                                       //  not executed, avoid gcc warning
}


//  algorithm 2

void sketch_algorithm2()
{
   using namespace sketch_names;

   int         px, py, qx, qy;
   int         ii, jj, kk, dist;
   int         br0, br1, br2, br3, move;
   int         bright, cont, brcon;

   for (py = 0; py < hh; py++)                                                   //  convert to monocolor image
   for (px = 0; px < ww; px++)                                                   //    with brightness range 0-255
   {
      ii = py * ww + px;

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

      bright = Bweight * britness[ii];                                           //  brightness 0-255
      if (bright > Brthresh) bright = 255;
      cont = CLcont[ii];                                                         //  contrast 0-255

      brcon = (1.0 - Cweight) * bright - Cweight * cont;                         //  255 = max. brightness + min. contrast
      if (brcon < 0) brcon = 0;
      if (brcon > 0) brcon = 255;                                                //  pixels are black/white
      monopix[ii] = brcon;                                                       //  0-255
   }

   for (br0 = 5; br0 <= 255; br0 += 50)                                          //  loop brightness threshold
   {
      for (py = 1; py < hh-1; py++)                                              //  loop image interior pixels
      for (px = 1; px < ww-1; px++)
      {
         ii = py * ww + px;

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

         br1 = monopix[ii];
         if (br1 > br0 || br1 == 0) continue;                                    //  skip target pixel > threshold or black

         for (qy = py-1; qy <= py+1; qy++)                                       //  loop pixels within 1 of target pixel
         for (qx = px-1; qx <= px+1; qx++)
         {
            jj = qy * ww + qx;
            br2 = monopix[jj];
            if (br2 == 0) continue;                                              //  ignore black pixels
            if (br2 < br1) goto nextpixel;                                       //  target pixel not darkest
         }

         br2 = 0;
         kk = -1;

         for (qy = py-1; qy <= py+1; qy++)                                       //  loop pixels within 1 of target pixel
         for (qx = px-1; qx <= px+1; qx++)
         {
            jj = qy * ww + qx;
            br3 = monopix[jj];
            if (br3 < 255 && br3 > br2) {                                        //  find brightest < 255
               br2 = br3;
               kk = jj;
            }
         }

         if (kk < 0) continue;

         move = br1;                                                             //  move darkness to target pixel,
         if (255 - br2 < move) move = 255 - br2;                                 //    as much as possible
         br1 -= move;
         br2 += move;
         monopix[ii] = br1;
         monopix[kk] = br2;

         nextpixel: ;
      }
   }

   return;                                                                       //  not executed, avoid gcc warning
}


//  convert monopix[*] to final image
//  black: chosen color
//  white: white

void sketch_finish()
{
   using namespace sketch_names;

   int         px, py, qx, qy;
   int         ii, jj, dist;
   int         br1, brsum;
   int         R, G, B;
   float       *pix1, *pix3, f1, f2;
   
   memset(fclip,0,cc);

   for (py = 1; py < hh-1; py++)                                                 //  loop all pixels
   for (px = 1; px < ww-1; px++)                                                 //  (interior pixels only)
   {
      ii = py * ww + px;

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

      brsum = 0;

      for (qy = py-1; qy <= py+1; qy++)                                          //  loop pixels within 1 of target pixel
      for (qx = px-1; qx <= px+1; qx++)
      {
         jj = qy * ww + qx;                                                      //  sum brightness
         brsum += monopix[jj];
      }

      brsum = brsum / 9;                                                         //  average brightness

      if (brsum > cliplev) fclip[ii] = 1;                                        //  reduce isolated dark pixels
   }

   for (ii = 0; ii < cc; ii++)                                                   //  clipped pixels >> white
      if (fclip[ii]) monopix[ii] = 255;

   for (py = 0; py < hh; py++)                                                   //  loop all pixels
   for (px = 0; px < ww; px++)
   {
      ii = py * ww + px;

      br1 = monopix[ii];                                                         //  input pixel brightness 0-255
      f1 = 0.003906 * br1;                                                       //  background part, 0 - 1
      f2 = 1.0 - f1;                                                             //  foreground part, 1 - 0
      
      R = f2 * fgrgb[0] + f1 * bgrgb[0];                                         //  foreground + background
      G = f2 * fgrgb[1] + f1 * bgrgb[1];
      B = f2 * fgrgb[2] + f1 * bgrgb[2];

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

      if (sa_stat == 3) {                                                        //  select area active
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel outside area
         if (dist < sa_blendwidth) {
            pix1 = PXMpix(E1pxm,px,py);                                          //  input pixel (blend area)
            f1 = sa_blendfunc(dist);
            f2 = 1.0 - f1;
            pix3[0] = f1 * R + f2 * pix1[0];                                     //  blend monocolor brightness
            pix3[1] = f1 * G + f2 * pix1[1];                                     //    and original image
            pix3[2] = f1 * B + f2 * pix1[2];
            continue;
         }
      }

      pix3[0] = R;
      pix3[1] = G;
      pix3[2] = B;
   }

   return;
}


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

//  convert an image into a cartoon drawing

namespace cartoon
{
   editfunc    EFcartoon;
   int         line_threshold = 1000;
   int         line_width = 1;
   int         blur_radius = 1;
   int         kuwahara_depth = 1;
   int         priorKD, priorBR;
   int         ww, hh;
   float       *pixcon;
   float       Fthreshold;
   float       Wrad[21][21];                                                     //  radius <= 10
   
   int    dialog_event(zdialog* zd, cchar *event);
   void * thread(void *);
   void   blur();
   void * blur_wthread(void *);
   void   kuwahara();
   void * kuwahara_wthread(void *);
   void   contrast_map();
   void   drawlines();
   void * drawlines_wthread(void *);
}


//  menu function

void m_cartoon(GtkWidget *, cchar *menu)
{
   using namespace cartoon;

   F1_help_topic = "cartoon";

   EFcartoon.menuname = menu;
   EFcartoon.menufunc = m_cartoon;
   EFcartoon.funcname = "cartoon";
   EFcartoon.Farea = 2;                                                          //  select area usable
   EFcartoon.Fscript = 1;                                                        //  scripting supported
   EFcartoon.threadfunc = thread;                                                //  thread function
   if (! edit_setup(EFcartoon)) return;                                          //  setup edit
   
   ww = E1pxm->ww;
   hh = E1pxm->hh;
   
   pixcon = (float *) zmalloc(ww * hh * sizeof(float));
   
   E8pxm = PXM_copy(E1pxm);                                                      //  blurred image
   E9pxm = PXM_copy(E1pxm);                                                      //  kuwahara image

/***
       _____________________________
      |         Cartoon             |
      |                             |
      |  Line Threshold  [ 20 ]     |
      |   Line Width     [  3 ]     |
      |   Blur Radius    [ 10 ]     |
      |  Kuwahara Depth  [  4 ]     |
      |                             |
      |     [Apply] [Done] [Cancel] |
      |_____________________________|
      
***/

   zdialog *zd = zdialog_new("Cartoon",Mwin,Bapply,Bdone,Bcancel,null);
   EFcartoon.zd = zd;

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

   zdialog_add_widget(zd,"label","lab1","vb1",E2X("Line Threshold"));
   zdialog_add_widget(zd,"label","lab1","vb1",E2X("Line Width"));
   zdialog_add_widget(zd,"label","lab2","vb1",E2X("Blur Radius"));
   zdialog_add_widget(zd,"label","lab1","vb1",E2X("Kuwahara Depth"));

   zdialog_add_widget(zd,"zspin","line_threshold","vb2","0|1000|1|1000");
   zdialog_add_widget(zd,"zspin","line_width","vb2","0|10|1|1");
   zdialog_add_widget(zd,"zspin","blur_radius","vb2","0|10|1|1");
   zdialog_add_widget(zd,"zspin","kuwahara_depth","vb2","0|10|1|1");
   
   zdialog_resize(zd,200,0);
   zdialog_restore_inputs(zd);
   zdialog_run(zd,dialog_event,"save");                                          //  run dialog, parallel

   zdialog_fetch(zd,"line_threshold",line_threshold);
   zdialog_fetch(zd,"line_width",line_width);
   zdialog_fetch(zd,"blur_radius",blur_radius);
   zdialog_fetch(zd,"kuwahara_depth",kuwahara_depth);
   
   priorKD = priorBR = -1;                                                       //  initial edit
   return;
}


//  dialog event and completion callback function

int cartoon::dialog_event(zdialog *zd, cchar *event)                             //  dialog event function
{
   using namespace cartoon;

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

   zdialog_fetch(zd,"line_threshold",line_threshold);                            //  get outline threshold 0-1000
   zdialog_fetch(zd,"line_width",line_width);                                    //  get line width 0-10
   zdialog_fetch(zd,"blur_radius",blur_radius);                                  //  get blur radius 0-10
   zdialog_fetch(zd,"kuwahara_depth",kuwahara_depth);                            //  get kuwahara depth 0-10

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  [apply]                            19.0
         zd->zstat = 0;                                                          //  keep dialog active
         signal_thread();
         return 1;
      }
      else if (zd->zstat == 2) edit_done(0);                                     //  [done]
      else edit_cancel(0);                                                       //  discard edit
      zfree(pixcon);
   }

   return 1;
}


//  thread function to update image

void * cartoon::thread(void *)
{
   using namespace cartoon;
   
   int      Fblur, Fkuwa, Fcon;
   
   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request
      
      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      Fblur = Fkuwa = Fcon = 0;
      
      if (blur_radius != priorBR) Fblur = Fkuwa = Fcon = 1;
      if (kuwahara_depth != priorKD) Fkuwa = Fcon = 1;

      priorBR = blur_radius;                                                     //  capture now, can change during process
      priorKD = kuwahara_depth;
      
      if (Fblur) blur();                                                         //  E1 + blur >> E8
      if (Fkuwa) kuwahara();                                                     //  E8 + kuwahara >> E9
      if (Fcon) contrast_map();                                                  //  compute E9 pixel contrast

      drawlines();                                                               //  draw lines where contrast > threshold
      
      CEF->Fmods++;
      CEF->Fsaved = 0;

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

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


//  Blur the image using blur_radius. E1 + blur >> E8

void cartoon::blur()
{
   using namespace cartoon;
   
   int      rad, px, py, qx, qy;
   float    R, W;
   
   PXM_free(E8pxm);
   E8pxm = PXM_copy(E1pxm);

   if (blur_radius == 0) return;

   rad = blur_radius;
   
   for (qy = -rad; qy <= rad; qy++)                                              //  compute blur weights for pixels 
   for (qx = -rad; qx <= rad; qx++)                                              //    within rad of target pixel
   {
      R = sqrtf(qx * qx + qy * qy);
      if (R > rad) W = 0.0;
      else if (R == 0) W = 1.0;
      else W = 1.0 / R;
      py = qy + rad;
      px = qx + rad;
      Wrad[py][px] = W;
   }
   
   do_wthreads(blur_wthread,NWT);                                                //  worker threads

   return;
}


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

   int      index = *((int *) arg);
   int      rad = blur_radius;
   int      ii, dist = 0, px, py, qx, qy;
   float    W, Wsum, f1, f2;
   float    R, G, B, *pix1, *pixq, *pix8;

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

      for (qy = -rad; qy <= rad; qy++)                                           //  compute weighted sum RGB for pixels
      for (qx = -rad; qx <= rad; qx++)                                           //    within rad of target pixel
      {
         pix1 = PXMpix(E1pxm,px,py);
         pixq = PXMpix(E1pxm,px+qx,py+qy);
         if (PIXMATCH(pix1,pixq) < 0.7) continue;                                //  use only pixels matching target pixel
         W = Wrad[qy+rad][qx+rad];
         R += W * pixq[0];
         G += W * pixq[1];
         B += W * pixq[2];
         Wsum += W;
      }
      
      R = R / Wsum;                                                              //  divide by sum of weights
      G = G / Wsum;
      B = B / Wsum;

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

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

   pthread_exit(0);
}


//  Perform a kuwahara sharpen of the image. E8 + kuwahara >> E9.

void cartoon::kuwahara()
{
   using namespace cartoon;
   
   PXM_free(E9pxm);
   E9pxm = PXM_copy(E8pxm);
   
   if (kuwahara_depth == 0) return;

   do_wthreads(kuwahara_wthread,NWT);                                            //  worker threads

   return;
}


void * cartoon::kuwahara_wthread(void *arg)                                      //  worker thread function
{
   using namespace cartoon;

   int      index = *((int *) arg);
   int      px, py, qx, qy, rx, ry;
   int      ii, rad, N, dist = 0;
   float    *pix1, *pix8, *pix9;
   float    red, green, blue, red2, green2, blue2;
   float    vmin, vall, vred, vgreen, vblue;
   float    red9, green9, blue9;
   float    f1, f2;

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

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

      vmin = 99999;
      red9 = green9 = blue9 = 0;

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

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

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

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

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

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

      pix9 = PXMpix(E9pxm,px,py);                                                //  output pixel
      pix9[0] = red9;
      pix9[1] = green9;
      pix9[2] = blue9;
   }

   pthread_exit(0);
}


//  Map contrast for E9 image pixels and set threshold scale.

void cartoon::contrast_map()
{
   using namespace cartoon;

   int      ii, px, py, qx, qy;
   double   sumcon, pix1con, maxcon;
   float    *pixx, *pixq;
   
   maxcon = 0;

   for (py = 1; py < hh-1; py++)                                                 //  make image pixel contrast map
   for (px = 1; px < ww-1; px++)
   {
      sumcon = 0;

      for (qy = py-1; qy <= py+1; qy++)
      for (qx = px-1; qx <= px+1; qx++)
      {
         pixx = PXMpix(E9pxm,px,py);                                             //  pixel contrast is mean contrast
         pixq = PXMpix(E9pxm,qx,qy);                                             //    with 8 neighboring pixels
         sumcon += fabsf(pixx[0] - pixq[0]);
         sumcon += fabsf(pixx[1] - pixq[1]);
         sumcon += fabsf(pixx[2] - pixq[2]);
      }
      
      ii = py * ww + px;
      pixcon[ii] = pix1con = 0.0417 * sumcon;                                    //  / (8 * 3) 
      if (pix1con > maxcon) maxcon = pix1con;
   }
   
   Fthreshold = maxcon;                                                          //  threshold scale factor
   return;
}


//  Draw lines on the image where edges are detected.
//  E9 >> E3, add lines to E3, use E1 for area blend.

void cartoon::drawlines()
{
   PXM_free(E3pxm);
   E3pxm = PXM_copy(E9pxm);
   do_wthreads(drawlines_wthread,NWT);                                           //  worker threads
}


void * cartoon::drawlines_wthread(void *arg)                                     //  worker thread function
{
   using namespace cartoon;

   int         index = *((int *) arg);
   int         px, py, qx, qy, ii, dist = 0;
   float       Fthresh, paint, f1, f2;
   float       *pix1, *pix3, *pix3q;
   
   Fthresh = 0.001 * line_threshold * Fthreshold;                                //  0 .. 1000  >>  0 .. Fthreshold

   for (py = index+1; py < hh-1; py += NWT)
   for (px = 1; px < ww-1; px++)
   {
      ii = py * ww + px;

      if (sa_stat == 3) {                                                        //  select area active
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel outside area
      }
      
      if (pixcon[ii] < Fthresh) continue;
      
      pix1 = PXMpix(E1pxm,px,py);                                                //  input image pixel (for area blend)
      pix3 = PXMpix(E3pxm,px,py);                                                //  output image pixel

      paint = line_width * (pixcon[ii] - Fthresh) / Fthresh;
      
      if (paint <= 1) {
         f1 = paint;
         f2 = 1.0 - f1;
         pix3[0] = f2 * pix3[0];
         pix3[1] = f2 * pix3[1];
         pix3[2] = f2 * pix3[2];
      }
      
      else {
         f1 = (paint - 1.0) / 8.0;
         if (f1 > 1.0) f1 = 1.0;
         f2 = 1.0 - f1;

         for (qy = py-1; qy <= py+1; qy++)
         for (qx = px-1; qx <= px+1; qx++)
         {
            if (qx == px && qy == py)
               pix3[0] = pix3[1] = pix3[2] = 0;
            else {
               pix3q = PXMpix(E3pxm,qx,qy);
               pix3q[0] = f2 * pix3q[0];
               pix3q[1] = f2 * pix3q[1];
               pix3q[2] = f2 * pix3q[2];
            }
         }
      }

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

   pthread_exit(0);
}


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

//  make an image outline drawing from edge pixels
//  superimpose drawing and image and vary brightness of each

namespace linedraw_names
{
   editfunc    EFlinedraw;
   float       outline_thresh;                                                   //  contrast threshold to make a line
   float       outline_width;                                                    //  drawn line width
   float       image_briteness;                                                  //  background image brightness
   int         blackwhite, negative;                                             //  flags
}

void m_line_drawing(GtkWidget *, cchar *menu)
{
   using namespace linedraw_names;

   int    linedraw_dialog_event(zdialog* zd, cchar *event);
   void * linedraw_thread(void *);

   F1_help_topic = "line drawing";

   EFlinedraw.menuname = menu;
   EFlinedraw.menufunc = m_line_drawing;
   EFlinedraw.funcname = "linedraw";
   EFlinedraw.Farea = 2;                                                         //  select area usable
   EFlinedraw.Fscript = 1;                                                       //  scripting supported 
   EFlinedraw.threadfunc = linedraw_thread;                                      //  thread function
   if (! edit_setup(EFlinedraw)) return;                                         //  setup edit

/***
       ___________________________________
      |      Line Drawing                 |
      |                                   |
      | Threshold =======[]============== |
      | Width =================[]======== |
      | Brightness =========[]=========== |
      | [_] black/white  [_] negative     |
      |                                   |
      |                   [Done] [Cancel] |
      |___________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Line Drawing"),Mwin,Bdone,Bcancel,null);
   EFlinedraw.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0);
   zdialog_add_widget(zd,"label","lab1","hb1",Bthresh,"space=3");
   zdialog_add_widget(zd,"hscale","olth","hb1","0|100|1|90","expand|space=5");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0);
   zdialog_add_widget(zd,"label","lab2","hb2",Bwidth,"space=5");
   zdialog_add_widget(zd,"hscale","olww","hb2","0|100|1|50","expand|space=5");
   zdialog_add_widget(zd,"hbox","hb3","dialog",0);
   zdialog_add_widget(zd,"label","lab3","hb3",Bbrightness,"space=5");
   zdialog_add_widget(zd,"hscale","imbr","hb3","0|100|1|10","expand|space=5");
   zdialog_add_widget(zd,"hbox","hb4","dialog",0);
   zdialog_add_widget(zd,"check","blackwhite","hb4",E2X("black/white"),"space=5");
   zdialog_add_widget(zd,"label","space","hb4",0,"space=10");
   zdialog_add_widget(zd,"check","negative","hb4",Bnegative);

   outline_thresh = 90;
   outline_width = 50;
   image_briteness = 10;
   blackwhite = negative = 0;

   zdialog_stuff(zd,"blackwhite",0);
   zdialog_stuff(zd,"negative",0);

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

   signal_thread();
   return;
}


//  dialog event and completion callback function

int linedraw_dialog_event(zdialog *zd, cchar *event)                             //  dialog event function
{
   using namespace linedraw_names;

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      PXM_free(E8pxm);
      PXM_free(E9pxm);
      return 1;
   }

   zdialog_fetch(zd,"olth",outline_thresh);                                      //  get outline threshold 0-100
   zdialog_fetch(zd,"olww",outline_width);                                       //  get outline width 0-100
   zdialog_fetch(zd,"imbr",image_briteness);                                     //  get image brightness 0-100
   zdialog_fetch(zd,"blackwhite",blackwhite);
   zdialog_fetch(zd,"negative",negative);

   signal_thread();
   return 1;
}


//  thread function to update image

void * linedraw_thread(void *)
{
   using namespace linedraw_names;

   void * linedraw_wthread(void *arg);

   int         px, py, ww, hh;
   float       olww, red3, green3, blue3;
   float       red3h, red3v, green3h, green3v, blue3h, blue3v;
   float       *pix8, *pix9;
   float       *pixa, *pixb, *pixc, *pixd, *pixe, *pixf, *pixg, *pixh;
   int         nc = E1pxm->nc;

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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      olww = 0.01 * outline_width;                                               //  outline width, 0 - 1.0
      olww = 1.0 - olww;                                                         //  1.0 - 0
      olww = 0.8 * olww + 0.2;                                                   //  1.0 - 0.2

      ww = E3pxm->ww * olww + 0.5;                                               //  create smaller outline image
      hh = E3pxm->hh * olww + 0.5;

      if (! E9pxm || ww != E9pxm->ww)                                            //  initial or changed outline brightness
      {
         PXM_free(E8pxm);
         PXM_free(E9pxm);

         E8pxm = PXM_rescale(E1pxm,ww,hh);
         E9pxm = PXM_copy(E8pxm);

         for (py = 1; py < hh-1; py++)
         for (px = 1; px < ww-1; px++)
         {
            pix8 = PXMpix(E8pxm,px,py);                                          //  input pixel
            pix9 = PXMpix(E9pxm,px,py);                                          //  output pixel

            pixa = pix8-nc*ww-nc;                                                //  8 neighboring pixels are used
            pixb = pix8-nc*ww;                                                   //    to get edge brightness using
            pixc = pix8-nc*ww+nc;                                                //       a Sobel filter
            pixd = pix8-nc;
            pixe = pix8+nc;
            pixf = pix8+nc*ww-nc;
            pixg = pix8+nc*ww;
            pixh = pix8+nc*ww+nc;

            red3h = -pixa[0] - 2 * pixb[0] - pixc[0] + pixf[0] + 2 * pixg[0] + pixh[0];
            red3v = -pixa[0] - 2 * pixd[0] - pixf[0] + pixc[0] + 2 * pixe[0] + pixh[0];
            green3h = -pixa[1] - 2 * pixb[1] - pixc[1] + pixf[1] + 2 * pixg[1] + pixh[1];
            green3v = -pixa[1] - 2 * pixd[1] - pixf[1] + pixc[1] + 2 * pixe[1] + pixh[1];
            blue3h = -pixa[2] - 2 * pixb[2] - pixc[2] + pixf[2] + 2 * pixg[2] + pixh[2];
            blue3v = -pixa[2] - 2 * pixd[2] - pixf[2] + pixc[2] + 2 * pixe[2] + pixh[2];

            red3 = (fabsf(red3h) + fabsf(red3v)) / 2;                            //  average vert. and horz. brightness
            green3 = (fabsf(green3h) + fabsf(green3v)) / 2;
            blue3 = (fabsf(blue3h) + fabsf(blue3v)) / 2;

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

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

      PXM_free(E8pxm);                                                           //  scale back to full-size
      E8pxm = PXM_rescale(E9pxm,E3pxm->ww,E3pxm->hh);

      do_wthreads(linedraw_wthread,NWT);                                         //  worker threads

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

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

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


void * linedraw_wthread(void *arg)                                               //  worker thread function
{
   using namespace linedraw_names;

   int         index = *((int *) arg);
   int         px, py, ii, dist = 0;
   float       olth, imbr, br8, dold, dnew;
   float       red1, green1, blue1, red3, green3, blue3;
   float       *pix1, *pix8, *pix3;

   olth = 0.01 * outline_thresh;                                                 //  outline threshold, 0 - 1.0
   olth = 1.0 - olth;                                                            //                     1.0 - 0
   imbr = 0.01 * image_briteness;                                                //  image brightness,  0 - 1.0

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

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

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

      br8 = pixbright(pix8);                                                     //  outline brightness
      br8 = br8 / 256.0;                                                         //  scale 0 - 1.0

      if (br8 > olth) {                                                          //  > threshold, use outline pixel
         red3 = pix8[0];
         green3 = pix8[1];
         blue3 = pix8[2];
      }
      else {                                                                     //  use image pixel dimmed by user control
         red3 = red1 * imbr;
         green3 = green1 * imbr;
         blue3 = blue1 * imbr;
      }

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

      if (blackwhite)
         red3 = green3 = blue3 = 0.333 * (red3 + green3 + blue3);

      if (negative) {
         red3 = 255.9 - red3;
         green3 = 255.9 - green3;
         blue3 = 255.9 - blue3;
      }

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

   pthread_exit(0);
}


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

//  convert image to simulate an embossing

int      emboss_radius, emboss_color;
float    emboss_depth;
float    emboss_kernel[20][20];                                                  //  support radius <= 9

editfunc    EFemboss;


//  menu function

void m_emboss(GtkWidget *, cchar *menu)
{
   int    emboss_dialog_event(zdialog* zd, cchar *event);
   void * emboss_thread(void *);

   F1_help_topic = "emboss";

   EFemboss.menuname = menu;
   EFemboss.menufunc = m_emboss;
   EFemboss.funcname = "emboss";
   EFemboss.Fscript = 1;                                                         //  scripting supported
   EFemboss.Farea = 2;                                                           //  select area usable
   EFemboss.threadfunc = emboss_thread;                                          //  thread function

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

/***
          ____________________________________
         |           Emboss                   |
         |                                    |
         | Radius [__]  Depth [__]  [x] Color |
         |                                    |
         |            [apply] [done] [cancel] |
         |____________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Emboss"),Mwin,Bapply,Bdone,Bcancel,null);
   EFemboss.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",Bradius,"space=5");
   zdialog_add_widget(zd,"zspin","radius","hb1","0|9|1|0");
   zdialog_add_widget(zd,"label","lab2","hb1",E2X("Depth"),"space=5");
   zdialog_add_widget(zd,"zspin","depth","hb1","0|99|1|0");
   zdialog_add_widget(zd,"check","color","hb1",Bcolor,"space=8");

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


//  dialog event and completion callback function

int emboss_dialog_event(zdialog *zd, cchar *event)                               //  emboss dialog event function
{
   if (strmatch(event,"focus")) return 1;
   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,"apply")) zd->zstat = 1;                                   //  from script                        20.0

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  [apply]
         zd->zstat = 0;                                                          //  keep dialog active
         zdialog_fetch(zd,"radius",emboss_radius);                               //  get user inputs
         zdialog_fetch(zd,"depth",emboss_depth);
         zdialog_fetch(zd,"color",emboss_color);
         signal_thread();                                                        //  trigger update thread
      }

      else if (zd->zstat == 2) edit_done(0);                                     //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   return 1;
}


//  thread function - use multiple working threads

void * emboss_thread(void *)
{
   void  * emboss_wthread(void *arg);

   int         dx, dy, rad;
   float       depth, kern, coeff;

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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      rad = emboss_radius;
      depth = emboss_depth;

      coeff = 0.1 * depth / (rad * rad + 1);

      for (dy = -rad; dy <= rad; dy++)                                           //  build kernel with radius and depth
      for (dx = -rad; dx <= rad; dx++)
      {
         kern = coeff * (dx + dy);
         emboss_kernel[dx+rad][dy+rad] = kern;
      }

      emboss_kernel[rad][rad] = 1;                                               //  kernel center cell = 1

      do_wthreads(emboss_wthread,NWT);                                           //  worker threads

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

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

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


void * emboss_wthread(void *arg)                                                 //  worker thread function
{
   void  emboss_1pix(int px, int py);

   int         index = *((int *) (arg));
   int         px, py;

   for (py = index; py < E3pxm->hh; py += NWT)                                   //  process all pixels
   for (px = 0; px < E3pxm->ww; px++)
      emboss_1pix(px,py);

   pthread_exit(0);
}


void emboss_1pix(int px, int py)                                                 //  process one pixel
{
   int         ii, dist = 0;
   float       bright1, bright3;
   int         rgb, dx, dy, rad;
   float       *pix1, *pix3, *pixN;
   float       sumpix, kern, dold, dnew;
   int         nc = E1pxm->nc;

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

   rad = emboss_radius;

   if (px < rad || py < rad) return;
   if (px > E3pxm->ww-rad-1 || py > E3pxm->hh-rad-1) return;

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

   if (emboss_color)
   {
      for (rgb = 0; rgb < 3; rgb++)
      {
         sumpix = 0;

         for (dy = -rad; dy <= rad; dy++)                                        //  loop surrounding block of pixels
         for (dx = -rad; dx <= rad; dx++)
         {
            pixN = pix1 + (dy * E1pxm->ww + dx) * nc;
            kern = emboss_kernel[dx+rad][dy+rad];
            sumpix += kern * pixN[rgb];

            bright1 = pix1[rgb];
            bright3 = sumpix;
            if (bright3 < 0) bright3 = 0;
            if (bright3 > 255) bright3 = 255;

            if (sa_stat == 3 && dist < sa_blendwidth) {                          //  select area is active,
               dnew = sa_blendfunc(dist);                                        //    blend changes over sa_blendwidth
               dold = 1.0 - dnew;
               bright3 = dnew * bright3 + dold * bright1;
            }

            pix3[rgb] = bright3;
         }
      }
   }

   else                                                                          //  use gray scale
   {
      sumpix = 0;

      for (dy = -rad; dy <= rad; dy++)                                           //  loop surrounding block of pixels
      for (dx = -rad; dx <= rad; dx++)
      {
         pixN = pix1 + (dy * E1pxm->ww + dx) * nc;
         kern = emboss_kernel[dx+rad][dy+rad];
         sumpix += kern * (pixN[0] + pixN[1] + pixN[2]);
      }

      bright1 = 0.3333 * (pix1[0] + pix1[1] + pix1[2]);
      bright3 = 0.3333 * sumpix;
      if (bright3 < 0) bright3 = 0;
      if (bright3 > 255) bright3 = 255;

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         dnew = sa_blendfunc(dist);                                              //    blend changes over sa_blendwidth
         dold = 1.0 - dnew;
         bright3 = dnew * bright3 + dold * bright1;
      }

      pix3[0] = pix3[1] = pix3[2] = bright3;
   }

   return;
}


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

//  convert image to simulate square tiles

int         tile_size, tile_gap, tile_3D;
float       *tile_pixmap = 0;

editfunc    EFtiles;


void m_tiles(GtkWidget *, cchar *menu)
{
   int    tiles_dialog_event(zdialog *zd, cchar *event);
   void * tiles_thread(void *);

   F1_help_topic = "tiles";

   EFtiles.menuname = menu;
   EFtiles.menufunc = m_tiles;
   EFtiles.funcname = "tiles";
   EFtiles.Farea = 2;                                                            //  select area usable
   EFtiles.Fscript = 1;                                                          //  scripting supported
   EFtiles.threadfunc = tiles_thread;                                            //  thread function
   if (! edit_setup(EFtiles)) return;                                            //  setup edit

/***
       _____________________________
      |       Simulate Tiles        |
      |                             |
      | tile size [____]            |
      | tile gap  [____]            |
      | 3D depth  [____]            |
      |                             |
      |     [Apply] [Done] [Cancel] |
      |_____________________________|

***/

   zdialog *zd = zdialog_new(E2X("Simulate Tiles"),Mwin,Bapply,Bdone,Bcancel,null);
   EFtiles.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labt","hb1",E2X("tile size"),"space=5");
   zdialog_add_widget(zd,"zspin","size","hb1","1|99|1|10","space=5");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labg","hb2",E2X("tile gap"),"space=5");
   zdialog_add_widget(zd,"zspin","gap","hb2","0|9|1|1","space=5");
   zdialog_add_widget(zd,"hbox","hb3","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labd","hb3",E2X("3D depth"),"space=5");
   zdialog_add_widget(zd,"zspin","3D","hb3","0|9|1|1","space=5");

   tile_size = 10;                                                               //  defaults
   tile_gap = 1;
   tile_3D = 0;

   int cc = E1pxm->ww * E1pxm->hh * 3 * sizeof(float);
   tile_pixmap = (float *) zmalloc(cc);                                          //  set up pixel color map
   memset(tile_pixmap,0,cc);

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


//  dialog event and completion callback function

int tiles_dialog_event(zdialog * zd, cchar *event)
{
   if (strmatch(event,"focus")) return 1;
   if (strmatch(event,"apply")) zd->zstat = 1;                                   //  from script
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()

   if (zd->zstat)                                                                //  dialog complete
   {
      if (zd->zstat == 1) {                                                      //  apply
         zd->zstat = 0;                                                          //  keep dialog active
         zdialog_fetch(zd,"size",tile_size);                                     //  get tile size
         zdialog_fetch(zd,"gap",tile_gap);                                       //  get tile gap
         zdialog_fetch(zd,"3D",tile_3D);                                         //  get 3D yes/no
         if (tile_size < 2) edit_reset();                                        //  restore original image
         else signal_thread();                                                   //  trigger working thread
         return 1;
      }
      else if (zd->zstat == 2) edit_done(0);                                     //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      zfree(tile_pixmap);
   }

   return 1;
}


//  image tiles thread function

void * tiles_thread(void *)
{
   int         sg, gg, eg, sumpix;
   int         ii, jj, px, py, qx, qy, dist;
   int         td, bd, ld, rd;
   float       red = 0, green = 0, blue = 0;
   float       hired = 0, higreen = 0, hiblue = 0;
   float       lored = 0, logreen = 0, loblue = 0;
   float       dnew, dold;
   float       *pix1, *pix3;

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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      sg = tile_size + tile_gap;
      gg = tile_gap;

      for (py = 0; py < E1pxm->hh; py += sg)                                     //  initz. pixel color map for
      for (px = 0; px < E1pxm->ww; px += sg)                                     //    given pixel size
      {
         sumpix = red = green = blue = 0;

         for (qy = py + gg; qy < py + sg; qy++)                                  //  get mean color for pixel block
         for (qx = px + gg; qx < px + sg; qx++)
         {
            if (qy > E1pxm->hh-1 || qx > E1pxm->ww-1) continue;

            pix1 = PXMpix(E1pxm,qx,qy);
            red += pix1[0];
            green += pix1[1];
            blue += pix1[2];
            sumpix++;
         }

         if (! sumpix) continue;

         red = (red / sumpix);                                                   //  tile color
         green = (green / sumpix);
         blue = (blue / sumpix);

         if (tile_3D)
         {
            hired = 0.5 * (red + 255.0);                                         //  3D highlight and shadow colors
            higreen = 0.5 * (green + 255.0);
            hiblue = 0.5 * (blue + 255.0);
            lored = 0.5 * red;
            logreen = 0.5 * green;
            loblue = 0.5 * blue;
         }

         for (qy = py; qy < py + sg; qy++)                                       //  set color for pixels in block
         for (qx = px; qx < px + sg; qx++)
         {
            if (qy > E1pxm->hh-1 || qx > E1pxm->ww-1) continue;

            jj = (qy * E1pxm->ww + qx) * 3;                                      //  pixel index

            td = qx - px;                                                        //  distance from top edge of tile
            bd = px + sg - qx;                                                   //  distance from bottom edge
            ld = qy - py;                                                        //  distance from left edge
            rd = py + sg - qy;                                                   //  distance from right edge

            if (td < gg || ld < gg) {                                            //  gap pixels are black
               tile_pixmap[jj] = tile_pixmap[jj+1] = tile_pixmap[jj+2] = 0;
               continue;
            }

            if (tile_3D)
            {
               eg = tile_3D;                                                     //  tile "depth"

               if (td < gg+eg && td < rd) {                                      //  top edge: highlight
                  tile_pixmap[jj] = hired;                                       //  bevel at right end
                  tile_pixmap[jj+1] = higreen;
                  tile_pixmap[jj+2] = hiblue;
                  continue;
               }

               if (ld < gg+eg && ld < bd) {                                      //  left edge: highlight
                  tile_pixmap[jj] = hired;                                       //  bevel at bottom end
                  tile_pixmap[jj+1] = higreen;
                  tile_pixmap[jj+2] = hiblue;
                  continue;
               }

               if (bd <= eg || rd <= eg) {                                       //  bottom or right edge: shadow
                  tile_pixmap[jj] = lored;
                  tile_pixmap[jj+1] = logreen;
                  tile_pixmap[jj+2] = loblue;
                  continue;
               }
            }

            tile_pixmap[jj] = red;
            tile_pixmap[jj+1] = green;
            tile_pixmap[jj+2] = blue;
         }
      }

      if (sa_stat == 3)                                                          //  process selected area
      {
         for (ii = 0; ii < E3pxm->ww * E3pxm->hh; ii++)                          //  find pixels in select area
         {
            if (! sa_pixmap[ii]) continue;
            py = ii / E3pxm->ww;
            px = ii - py * E3pxm->ww;
            dist = sa_pixmap[ii];                                                //  distance from edge
            pix3 = PXMpix(E3pxm,px,py);
            jj = (py * E3pxm->ww + px) * 3;

            if (dist >= sa_blendwidth) {                                         //  blend changes over sa_blendwidth
               pix3[0] = tile_pixmap[jj];
               pix3[1] = tile_pixmap[jj+1];                                      //  apply block color to member pixels
               pix3[2] = tile_pixmap[jj+2];
            }
            else {
               dnew = sa_blendfunc(dist);
               dold = 1.0 - dnew;
               pix1 = PXMpix(E1pxm,px,py);
               pix3[0] = dnew * tile_pixmap[jj] + dold * pix1[0];
               pix3[1] = dnew * tile_pixmap[jj+1] + dold * pix1[1];
               pix3[2] = dnew * tile_pixmap[jj+2] + dold * pix1[2];
            }
         }
      }

      else                                                                       //  process entire image
      {
         for (py = 0; py < E3pxm->hh-1; py++)                                    //  loop all image pixels
         for (px = 0; px < E3pxm->ww-1; px++)
         {
            pix3 = PXMpix(E3pxm,px,py);                                          //  target pixel
            jj = (py * E3pxm->ww + px) * 3;                                      //  color map for (px,py)
            pix3[0] = tile_pixmap[jj];
            pix3[1] = tile_pixmap[jj+1];
            pix3[2] = tile_pixmap[jj+2];
         }
      }

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

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

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


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

//  Dither menu, incorporating dither0, dither1, dither2, dither3

void m_dither(GtkWidget *, cchar *menu)
{
   int   dither_dialog_event(zdialog *zd, cchar *event);                         //  19.0
   
   zdialog  *zd;
   
   F1_help_topic = "dither";
   if (FGWM != 'F') return;
   if (! curr_file) return;
   
/***
          ____________________________________________
         |               Dither Image                 |
         |                                            |
         |  [Dither0]   Roy Lichtenstein effect       |
         |  [Dither1]   pure RGB or black/white dots  |
         |  [Dither2]   RGB mix with given bit-depth  |
         |  [Dither3]   custom palette colors         |
         |                                            |
         |                                     [done] |
         |____________________________________________|

***/

   zd = zdialog_new(E2X("Dither Image"),Mwin,Bdone,null);
   
   zdialog_add_widget(zd,"hbox","hb0","dialog");
   zdialog_add_widget(zd,"button","dither0","hb0",E2X("Dither0"),"space=3");
   zdialog_add_widget(zd,"label","lab0","hb0",E2X("Roy Lichtenstein effect"),"space=3");

   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"button","dither1","hb1",E2X("Dither1"),"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",E2X("pure RGB or black/white dots"),"space=3");

   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"button","dither2","hb2",E2X("Dither2"),"space=3");
   zdialog_add_widget(zd,"label","lab2","hb2",E2X("RGB mix with given bit-depth"),"space=3");

   zdialog_add_widget(zd,"hbox","hb3","dialog");
   zdialog_add_widget(zd,"button","dither3","hb3",E2X("Dither3"),"space=3");
   zdialog_add_widget(zd,"label","lab3","hb3",E2X("custom palette colors"),"space=3");
   
   zdialog_run(zd,dither_dialog_event,"mouse");
   return;
}


//  dialog event and completion function

int dither_dialog_event(zdialog *zd, cchar *event)
{
   if (zd->zstat) {                                                              //  done or [x]
      zdialog_free(zd);                                                          //  kill dialog
      return 1;
   }
   
   if (! zstrstr(event,"dither")) return 1;                                      //  ignore other events
   
   if (strmatch(event,"dither0")) m_dither0(0,0);
   if (strmatch(event,"dither1")) m_dither1(0,0);
   if (strmatch(event,"dither2")) m_dither2(0,0);
   if (strmatch(event,"dither3")) m_dither3(0,0);
   zdialog_free(zd); 

   return 1;
}


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

//  convert image to dot grid (like Roy Lichtenstein)

int         dot_size;                                                            //  tile size enclosing dots
editfunc    EFdither0;


void m_dither0(GtkWidget *, cchar *menu)
{
   int    dither0_dialog_event(zdialog *zd, cchar *event);
   void * dither0_thread(void *);

   F1_help_topic = "dither";

   EFdither0.menuname = "dither0"; 
   EFdither0.menufunc = m_dither0;
   EFdither0.funcname = "dither0";
   EFdither0.Farea = 2;                                                          //  select area usable
   EFdither0.Fscript = 1;                                                        //  scripting supported                20.0
   EFdither0.threadfunc = dither0_thread;                                        //  thread function

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

   zdialog *zd = zdialog_new(E2X("Roy Lichtenstein effect"),Mwin,Bapply,Bdone,Bcancel,null);
   EFdither0.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labt","hb1",E2X("dot size"),"space=5");
   zdialog_add_widget(zd,"zspin","size","hb1","4|99|1|9","space=5");

   dot_size = 9;

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


//  dialog event and completion callback function

int dither0_dialog_event(zdialog * zd, cchar *event)
{
   if (strmatch(event,"focus")) return 1;
   if (strmatch(event,"apply")) zd->zstat = 1;                                   //  from script
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  apply
         zd->zstat = 0;                                                          //  keep dialog active
         zdialog_fetch(zd,"size",dot_size);                                      //  get dot size
         signal_thread();                                                        //  trigger working thread
      }
      else if (zd->zstat == 2) edit_done(0);                                     //  commit edit
      else edit_cancel(0);                                                       //  discard edit
   }

   return 1;
}


//  image dither0 thread function

void * dither0_thread(void *)
{
   int         ds, ii, sumpix;
   int         dist, shift1, shift2;
   int         px, py, px1, px2, py1, py2;
   int         qx, qy, qx1, qx2, qy1, qy2;
   float       *pix1, *pix3;
   float       red, green, blue, maxrgb;
   float       relmax, radius, radius2a, radius2b, f1, f2;
   float       fpi = 1.0 / 3.14159;
   float       dcx, dcy, qcx, qcy, qrad2;
   int         nc = E3pxm->nc;

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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paints                19.0

      ds = dot_size;                                                             //  dot size and tile size
      
      for (py = 0; py < E3pxm->hh; py++)                                         //  set image to black
      for (px = 0; px < E3pxm->ww; px++) {                                       //  not alpha channel
         pix3 = PXMpix(E3pxm,px,py);
         pix3[0] = pix3[1] = pix3[2] = 0;
      }

      px1 = 0;                                                                   //  limits for tiles
      px2 = E3pxm->ww - ds;
      py1 = 0;
      py2 = E3pxm->hh - ds;

      if (sa_stat == 3) {                                                        //  reduce for active area
         px1 = sa_minx;
         px2 = sa_maxx;
         py1 = sa_miny;
         py2 = sa_maxy;
         if (px2 > E3pxm->ww - ds) px2 = E3pxm->ww - ds;
         if (py2 > E3pxm->hh - ds) py2 = E3pxm->hh - ds;
      }

      shift1 = 0;

      for (py = py1; py < py2; py += ds)                                         //  loop all tiles in input image
      {
         shift1 = 1 - shift1;                                                    //  offset alternate rows
         shift2 = 0.5 * shift1 * ds;

         for (px = px1 + shift2; px < px2; px += ds)
         {
            sumpix = red = green = blue = 0;

            for (qy = py; qy < py + ds; qy++)                                    //  loop all pixels in tile
            for (qx = px; qx < px + ds; qx++)                                    //  get mean RGB levels for tile
            {
               pix1 = PXMpix(E1pxm,qx,qy);
               red += pix1[0];
               green += pix1[1];
               blue += pix1[2];
               sumpix++;
            }

            red = red / sumpix;                                                  //  mean RGB levels, 0 to 255.9
            green = green / sumpix;
            blue = blue / sumpix;

            maxrgb = red;                                                        //  max. mean RGB level, 0 to 255.9
            if (green > maxrgb) maxrgb = green;
            if (blue > maxrgb) maxrgb = blue;
            relmax = maxrgb / 256.0;                                             //  max. RGB as 0 to 0.999
            relmax = pow(relmax,1.6);                                            //  improve contrast                   19.0

            radius = ds * sqrt(fpi * relmax);                                    //  radius of dot with maximized color
            radius = radius * 0.9;                                               //  deliberate reduction
            if (radius < 0.5) continue;                                          //  tiny - leave tile black

            red = 255.9 * red / maxrgb;                                          //  dot color, maximized
            green = 255.9 * green / maxrgb;
            blue = 255.9 * blue / maxrgb;

            dcx = px + 0.5 * ds;                                                 //  center of dot / tile
            dcy = py + 0.5 * ds;

            qx1 = dcx - radius;                                                  //  pixels within dot radius of center
            qx2 = dcx + radius;
            qy1 = dcy - radius;
            qy2 = dcy + radius;

            radius2a = (radius + 0.5) * (radius + 0.5);
            radius2b = (radius - 0.5) * (radius - 0.5);

            for (qy = qy1; qy <= qy2; qy++)                                      //  loop all pixels within dot radius
            for (qx = qx1; qx <= qx2; qx++)
            {
               qcx = qx + 0.5;                                                   //  center of pixel
               qcy = qy + 0.5;
               qrad2 = (qcx-dcx)*(qcx-dcx) + (qcy-dcy)*(qcy-dcy);                //  pixel distance**2 from center of dot
               if (qrad2 > radius2a) f1 = 0.0;                                   //  pixel outside dot, no color
               else if (qrad2 < radius2b) f1 = 1.0;                              //  pixel inside dot, full color
               else f1 = radius + 0.5 - sqrtf(qrad2);                            //  pixel straddles edge, some color
               pix3 = PXMpix(E3pxm,qx,qy);
               pix3[0] = f1 * red;
               pix3[1] = f1 * green;
               pix3[2] = f1 * blue;
            }
         }
      }

      if (sa_stat == 3)                                                          //  select area active
      {
         pix1 = E1pxm->pixels;
         pix3 = E3pxm->pixels;

         for (ii = 0; ii < E3pxm->ww * E3pxm->hh; ii++)                          //  loop all pixels
         {
            dist = sa_pixmap[ii];
            if (dist == 0)                                                       //  outside area
               memmove(pix3,pix1,3*sizeof(float));
            else if (dist < sa_blendwidth) {
               f1 = sa_blendfunc(dist);                                          //  pixel in blend region
               f2 = 1.0 - f1;
               pix3[0] = f1 * pix3[0] + f2 * pix1[0];                            //  output pixel is mix of input and output
               pix3[1] = f1 * pix3[1] + f2 * pix1[1];
               pix3[2] = f1 * pix3[2] + f2 * pix1[2];
            }
            pix1 += nc;
            pix3 += nc;
         }
      }

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

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

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


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

//  Convert image into pure RGB or black/white dots, dithered to show other tones

namespace dither1_names
{
   editfunc    EFdither1;
   int         E3ww, E3hh;
   int         RES, RGB, BW;
   int         RAND;
}


//  menu function

void m_dither1(GtkWidget *, const char *menu)                                    //  19.0
{
   using namespace dither1_names;

   int    dither1_dialog_event(zdialog* zd, const char *event);
   void * dither1_thread(void *);

   F1_help_topic = "dither";

   EFdither1.menuname = "dither1";
   EFdither1.menufunc = m_dither1;
   EFdither1.funcname = "dither1";                                               //  function name
   EFdither1.threadfunc = dither1_thread;                                        //  thread function
   EFdither1.Farea = 2;                                                          //  select area usable
   EFdither1.Frestart = 1;                                                       //  allow restart
   EFdither1.FusePL = 1;                                                         //  can use with paint/lever edits
   EFdither1.Fscript = 1;                                                        //  scripting supported                20.0

   if (! edit_setup(EFdither1)) return;                                          //  setup edit
   
   E3ww = E3pxm->ww;
   E3hh = E3pxm->hh;

/***
       __________________________
      |        Dither1           |
      |                          |
      |  resolution  [ N ]       |          2x2 to 30x30                         //  see Pflag dependency below
      |  (o) RGB color           |
      |  (o) black/white         |
      |  [x] random position     |
      |                          |
      |  [apply] [done] [cancel] |
      |__________________________|
   
***/


   zdialog *zd = zdialog_new(E2X("Dither1"),Mwin,Bapply,Bdone,Bcancel,null);     //  dither1 dialog
   CEF->zd = zd;
   
   zdialog_add_widget(zd,"hbox","hbres","dialog");
   zdialog_add_widget(zd,"label","labres","hbres",E2X("resolution"),"space=5");
   zdialog_add_widget(zd,"zspin","RES","hbres","2|30|1|4","space=5");
   zdialog_add_widget(zd,"radio","RGB","dialog",E2X("RGB color"));
   zdialog_add_widget(zd,"radio","B&W","dialog",E2X("black/white"));
   zdialog_add_widget(zd,"check","RAND","dialog",E2X("random position"));

   zdialog_restore_inputs(zd);                                                   //  restore previous inputs
   zdialog_run(zd,dither1_dialog_event,"save");                                  //  run dialog - parallel

   return;
}


//  dither1 dialog event and completion function

int dither1_dialog_event(zdialog *zd, const char *event)
{
   using namespace dither1_names;

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

   zdialog_fetch(zd,"RES",RES);
   zdialog_fetch(zd,"RGB",RGB);
   zdialog_fetch(zd,"B&W",BW);
   zdialog_fetch(zd,"RAND",RAND);

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  apply
         zd->zstat = 0;
         signal_thread();
      }
      else if (zd->zstat == 2) edit_done(0);                                     //  done
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   if (strmatch(event,"blendwidth")) signal_thread();
   
   return 1;
}


//  thread function - multiple working threads to update image

void * dither1_thread(void *)
{
   using namespace dither1_names;

   void  * dither1_wthread_RGB(void *arg);
   void  * dither1_wthread_BW(void *arg);

   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request
      
      while (Fpaintrequest) zmainsleep(0.01); 
      paintlock(1);                                                              //  block window paint

      if (sa_stat == 3) Fbusy_goal = sa_Npixel;                                  //  set up progress monitor
      else  Fbusy_goal = E3pxm->ww * E3pxm->hh;
      Fbusy_done = 0;
      
      if (RGB) do_wthreads(dither1_wthread_RGB,NWT);                             //  do RGB worker threads
      if (BW) do_wthreads(dither1_wthread_BW,NWT);                               //  do B/W worker threads

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

      paintlock(0);                                                              //  unblock window paint
      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop warning
}


//  worker thread function for RGB dither1

void * dither1_wthread_RGB(void *arg)
{
   using namespace dither1_names;

   int         index = *((int *) arg);
   int         px, py, qx, qy;
   int         ii, jj, kk, dist = 1;
   int         pixcc = 3 * sizeof(float);
   int         RES2 = RES * RES;
   char        Pflag[1000];                                                      //  max. RES = 31
   float       *pix1, *pix3;
   float       Rs, Gs, Bs, Rp, Gp, Bp;
   float       maxrgb;
   float       f1, f2;

   pix3 = 0;                                                                     //  stop compiler 'uninitialized'
   kk = 0;                                                                       //    warnings

   for (py = index * RES; py < E3hh - RES; py += NWT * RES)                      //  loop each pixel block
   for (px = 0; px < E3ww - RES; px += RES)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }
      
      for (qy = py; qy < py + RES; qy++)                                         //  set all output pixels in block
      for (qx = px; qx < px + RES; qx++)                                         //    to RGB values 0/0/0
      {
         pix3 = PXMpix(E3pxm,qx,qy);
         memset(pix3,0,pixcc);
      }

      Rs = Gs = Bs = 0;
      
      for (qy = py; qy < py + RES; qy++)                                         //  get RGB sums for pixel block
      for (qx = px; qx < px + RES; qx++)
      {
         pix1 = PXMpix(E1pxm,qx,qy);
         Rs += pix1[0];
         Gs += pix1[1];
         Bs += pix1[2];
      }
      
      Rp = Rs / 256 / 3;                                                         //  count of dithered pixels for block
      Gp = Gs / 256 / 3;                                                         //  (single RGB value of 255)
      Bp = Bs / 256 / 3;

      memset(Pflag,0,RES2);                                                      //  reset pixel flags

      while (true)                                                               //  distribute dithered pixels
      {
         if (Rp < 0.5 && Gp < 0.5 && Bp < 0.5) break;                            //  nothing left to distribute

         ii = 0;                                                                 //  get greatest remaining
         if (Gp >= Rp && Gp >= Bp) ii = 1;                                       //    dithered pixel count
         if (Bp >= Rp && Bp >= Gp) ii = 2;

         if (ii == 0) Rp -= 1;                                                   //  decrement dithered pixel count
         if (ii == 1) Gp -= 1;
         if (ii == 2) Bp -= 1;
         
         if (RAND)                                                               //  use random position in block
         {
            kk = RES2 * drandz();                                                //  random initial search posn.

            for (jj = 0; jj < RES2; jj++)                                        //  loop pixels in block
            {
               kk++;                                                             //  next pixel
               if (kk == RES2) kk = 0;
               if (! Pflag[kk]) break;                                           //  available
            }
            
            if (jj < RES2)                                                       //  found
            {
               Pflag[kk] = 1;                                                    //  mark pixel as used
               qy = kk / RES;                                                    //  pixel within block
               qx = kk - RES * qy;
               pix3 = PXMpix(E3pxm, px + qx, py + qy);                           //  make into dithered pixel
               pix3[ii] = 255;
            }
         }
         
         else                                                                    //  use best match position in block
         {
            maxrgb = 0;

            for (jj = 0; jj < RES2; jj++)                                        //  loop pixels in block
            {
               if (Pflag[jj]) continue;                                          //  pixel used already
               qy = jj / RES;                                                    //  pixel within block
               qx = jj - qy * RES;
               pix1 = PXMpix(E1pxm, px + qx, py + qy);                           //  find pixel with greatest
               if (pix1[ii] > maxrgb) {                                          //    matching RGB value
                  maxrgb = pix1[ii]; 
                  kk = jj; 
               }
            }
         
            if (maxrgb > 0)                                                      //  found pixel
            {
               Pflag[kk] = 1;                                                    //  mark pixel as used
               qy = kk / RES;                                                    //  matching pixel within block
               qx = kk - RES * qy;
               pix3 = PXMpix(E3pxm, px + qx, py + qy);                           //  make into dithered pixel
               pix3[ii] = 255;
            }
         }
      }
      
      if (sa_stat == 3 && dist < sa_blendwidth) 
      {
         f1 = sa_blendfunc(dist);                                                //  blend changes over sa_blend
         f2 = 1.0 - f1;

         for (qy = py; qy < py + RES; qy++)
         for (qx = px; qx < px + RES; qx++)
         {
            pix1 = PXMpix(E1pxm,qx,qy);
            pix3 = PXMpix(E3pxm,qx,qy);
            pix3[0] = f1 * pix3[0] + f2 * pix1[0];
            pix3[1] = f1 * pix3[1] + f2 * pix1[1];
            pix3[2] = f1 * pix3[2] + f2 * pix1[2];
         }
      }

      Fbusy_done += RES2;
   }

   pthread_exit(0);                                                              //  exit thread
}


//  worker thread function for black/white dither

void * dither1_wthread_BW(void *arg)
{
   using namespace dither1_names;

   int         index = *((int *) arg);
   int         px, py, qx, qy;
   int         ii, jj, kk, dist = 1;
   int         pixcc = 3 * sizeof(float);
   int         RES2 = RES * RES;
   char        Pflag[1000];                                                      //  max. RES = 31
   float       *pix1, *pix3;
   float       white[3] = { 255, 255, 255 };
   float       Nw, Nb, minrgb, pixrgb;
   float       f1, f2;

   pix3 = 0;                                                                     //  stop compiler 'uninitialized'
   kk = 0;                                                                       //    warnings

   for (py = index * RES; py < E3hh - RES; py += NWT * RES)                      //  loop each pixel block
   for (px = 0; px < E3ww - RES; px += RES)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }
      
      for (qy = py; qy < py + RES; qy++)                                         //  set all output pixels in block
      for (qx = px; qx < px + RES; qx++)                                         //    to RGB white
      {
         pix3 = PXMpix(E3pxm,qx,qy);
         memcpy(pix3,white,pixcc);
      }

      Nw = 0;
      
      for (qy = py; qy < py + RES; qy++)                                         //  get RGB sum for pixel block
      for (qx = px; qx < px + RES; qx++)
      {
         pix1 = PXMpix(E1pxm,qx,qy);
         Nw += pix1[0] + pix1[1] + pix1[2];
      }
      
      Nw = Nw / 255 / 3;                                                         //  white pixel count
      Nb = RES2 - Nw;                                                            //  black pixel count
      
      memset(Pflag,0,RES2);                                                      //  reset pixel flags

      while (true)                                                               //  distribute black pixels
      {
         if (Nb < 0.5) break;                                                    //  nothing left to distribute
         Nb -= 1;

         minrgb = 9999;

         if (RAND)                                                               //  use random position in block
         {
            kk = RES2 * drandz();                                                //  random initial search posn.

            for (jj = 0; jj < RES2; jj++)                                        //  loop pixels in block
            {
               kk++;                                                             //  next pixel
               if (kk == RES2) kk = 0;
               if (! Pflag[kk]) break;                                           //  available
            }
            
            if (jj < RES2)                                                       //  found
            {
               Pflag[kk] = 1;                                                    //  mark pixel as used
               qy = kk / RES;                                                    //  pixel within block
               qx = kk - RES * qy;
               pix3 = PXMpix(E3pxm, px + qx, py + qy);
               pix3[0] = pix3[1] = pix3[2] = 0;                                  //  make pixel black
            }
         }
         
         else                                                                    //  use best match position in block
         {
            for (jj = 0; jj < RES2; jj++)                                        //  loop pixels in block
            {
               if (Pflag[jj]) continue;                                          //  pixel used already
               qy = jj / RES;                                                    //  pixel within block
               qx = jj - qy * RES;
               pix1 = PXMpix(E1pxm, px + qx, py + qy);
               pixrgb = pix1[0] + pix1[1] + pix1[2];                             //  get pixel RGB sum
               if (pixrgb < minrgb) {
                  minrgb = pixrgb;                                               //  remember pixel with lowest sum
                  kk = jj; 
               }
            }
            
            if (minrgb < 9999)                                                   //  found pixel
            {
               Pflag[kk] = 1;                                                    //  mark pixel as used
               qy = kk / RES;                                                    //  matching pixel within block
               qx = kk - RES * qy;
               pix3 = PXMpix(E3pxm, px + qx, py + qy);
               pix3[0] = pix3[1] = pix3[2] = 0;                                  //  make pixel black
            }
         }
      }
      
      if (sa_stat == 3 && dist < sa_blendwidth) 
      {
         f1 = sa_blendfunc(dist);                                                //  blend changes over sa_blend
         f2 = 1.0 - f1;

         for (qy = py; qy < py + RES; qy++)
         for (qx = px; qx < px + RES; qx++)
         {
            pix1 = PXMpix(E1pxm,qx,qy);
            pix3 = PXMpix(E3pxm,qx,qy);
            pix3[0] = f1 * pix3[0] + f2 * pix1[0];
            pix3[1] = f1 * pix3[1] + f2 * pix1[1];
            pix3[2] = f1 * pix3[2] + f2 * pix1[2];
         }
      }

      Fbusy_done += RES2;
   }

   pthread_exit(0);                                                              //  exit thread
}


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

//  Dither image using RGB colors and Floyd-Steinberg error distribution algorithm.

namespace dither2_names
{
   editfunc    EFdither2;
   int         E3ww, E3hh;
   int         RES, RES2, DEPTH, COMP;
}


//  menu function

void m_dither2(GtkWidget *, const char *menu)                                    //  19.0
{
   using namespace dither2_names;

   int    dither2_dialog_event(zdialog* zd, const char *event);
   void * dither2_thread(void *);

   F1_help_topic = "dither";

   EFdither2.menuname = "dither2";
   EFdither2.menufunc = m_dither2;
   EFdither2.funcname = "dither2";                                               //  function name
   EFdither2.threadfunc = dither2_thread;                                        //  thread function
   EFdither2.Farea = 2;                                                          //  select area usable
   EFdither2.Frestart = 1;                                                       //  allow restart
   EFdither2.FusePL = 1;                                                         //  can use with paint/lever edits
   EFdither2.Fscript = 1;                                                        //  scripting supported                20.0

   if (! edit_setup(EFdither2)) return;                                          //  setup edit
   
   E3ww = E3pxm->ww;
   E3hh = E3pxm->hh;

/***
       __________________________
      |        Dither2           |
      |                          |
      |  resolution  [ N ]       |          2x2 to 30x30
      |  color depth [ N ]       |          1 to 8 bits
      |  [x] error compensation  |
      |                          |
      |  [apply] [done] [cancel] |
      |__________________________|
   
***/


   zdialog *zd = zdialog_new(E2X("Dither2"),Mwin,Bapply,Bdone,Bcancel,null);     //  dither dialog
   CEF->zd = zd;
   
   zdialog_add_widget(zd,"hbox","hbres","dialog");
   zdialog_add_widget(zd,"label","labres","hbres",E2X("resolution"),"space=5");
   zdialog_add_widget(zd,"zspin","RES","hbres","2|30|1|4","space=5|size=2");
   zdialog_add_widget(zd,"hbox","hbdep","dialog");
   zdialog_add_widget(zd,"label","labdep","hbdep",E2X("color depth"),"space=5");
   zdialog_add_widget(zd,"zspin","DEPTH","hbdep","1|8|1|2","space=5|size=2");
   zdialog_add_widget(zd,"hbox","hbcomp","dialog");
   zdialog_add_widget(zd,"check","COMP","hbcomp",E2X("error compensation"),"space=5"); 

   zdialog_restore_inputs(zd);                                                   //  restore previous inputs
   zdialog_run(zd,dither2_dialog_event,"save");                                  //  run dialog - parallel

   return;
}


//  dither2 dialog event and completion function

int dither2_dialog_event(zdialog *zd, const char *event)
{
   using namespace dither2_names;

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

   zdialog_fetch(zd,"RES",RES);
   zdialog_fetch(zd,"DEPTH",DEPTH);
   zdialog_fetch(zd,"COMP",COMP);

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  apply
         zd->zstat = 0;
         signal_thread();
      }
      else if (zd->zstat == 2) edit_done(0);                                     //  done
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   if (strmatch(event,"blendwidth")) signal_thread();
   
   return 1;
}


//  thread function - multiple working threads to update image

void * dither2_thread(void *)
{
   using namespace dither2_names;

   int      bits[9] = { 1, 2, 4, 8, 16, 32, 64, 128, 256 };
   int      Brows, Bcols, Brow, Bcol, block, Nblocks;
   int      pxlo, pxhi, pylo, pyhi, px, py;
   float    *blockR, *blockG, *blockB;
   float    R, G, B, Rp, Gp, Bp, Re, Ge, Be;
   int      n1, n2, n3, n4;
   int      bb, ii, dist = 0;
   float    *pix1, *pix3;
   float    f1, f2;

   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request
      
      while (Fpaintrequest) zmainsleep(0.01); 
      paintlock(1);                                                              //  block window paint
      
      bb = bits[9-DEPTH];                                                        //  DEPTH: 1 .. 8   bb: 256 128 64 .. 2
      RES2 = RES * RES;                                                          //  pixel block size
      Bcols = E3ww / RES;                                                        //  block rows and cols
      Brows = E3hh / RES;

      Nblocks = Brows * Bcols;                                                   //  allocate block color memory
      blockR = (float *) zmalloc(Nblocks * sizeof(float));
      blockG = (float *) zmalloc(Nblocks * sizeof(float));
      blockB = (float *) zmalloc(Nblocks * sizeof(float));

      Fbusy_goal = Nblocks;
      Fbusy_done = 0;

      for (Brow = 0; Brow < Brows; Brow++)                                       //  loop blocks
      for (Bcol = 0; Bcol < Bcols; Bcol++)
      {
         block = Brow * Bcols + Bcol;                                            //  block sequence number

         pylo = Brow * RES;                                                      //  range of pixels in block
         pyhi = pylo + RES;
         pxlo = Bcol * RES;
         pxhi = pxlo + RES;
         
         R = G = B = 0;

         for (py = pylo; py < pyhi; py++)                                        //  loop pixels in block
         for (px = pxlo; px < pxhi; px++)
         {
            pix1 = PXMpix(E1pxm,px,py);                                          //  sum RGB for all pixels
            R += pix1[0];
            G += pix1[1];
            B += pix1[2];
         }
         
         blockR[block] = R / RES2;                                               //  save mean RGB values for block
         blockG[block] = G / RES2;
         blockB[block] = B / RES2;
      }

      if (COMP)
      {
         for (Brow = 1; Brow < Brows-1; Brow++)                                  //  loop blocks, omitting 
         for (Bcol = 1; Bcol < Bcols-1; Bcol++)                                  //    first/last rows/cols
         {
            block = Brow * Bcols + Bcol;                                         //  block sequence number
            
            R = blockR[block];                                                   //  block mean RGB
            G = blockG[block];
            B = blockB[block];

            Rp = int(R + bb/2) / bb * bb;                                        //  nearest palette RGB
            Gp = int(G + bb/2) / bb * bb;
            Bp = int(B + bb/2) / bb * bb;

            if (Rp > 255) Rp = 255;
            if (Gp > 255) Gp = 255;
            if (Bp > 255) Bp = 255;
            
            Re = R - Rp;                                                         //  errors
            Ge = G - Gp;
            Be = B - Bp;

            n1 = block + 1;                                                      //  distribute errors downstream
            n2 = block + Bcols - 1;
            n3 = n2 + 1;
            n4 = n3 + 1;
            
            blockR[n1] += 0.4375 * Re;
            blockG[n1] += 0.4375 * Ge;
            blockB[n1] += 0.4375 * Be;
            blockR[n2] += 0.1875 * Re;
            blockG[n2] += 0.1875 * Ge;
            blockB[n2] += 0.1875 * Be;
            blockR[n3] += 0.3125 * Re;
            blockG[n3] += 0.3125 * Ge;
            blockB[n3] += 0.3125 * Be;
            blockR[n4] += 0.0625 * Re;
            blockG[n4] += 0.0625 * Ge;
            blockB[n4] += 0.0625 * Be;
         }
      }

      for (Brow = 0; Brow < Brows; Brow++)                                       //  loop blocks
      for (Bcol = 0; Bcol < Bcols; Bcol++)
      {
         block = Brow * Bcols + Bcol;                                            //  block sequence number

         R = blockR[block];                                                      //  block mean RGB + errors
         G = blockG[block];
         B = blockB[block];

         Rp = int(R + bb/2) / bb * bb;                                           //  nearest palette RGB
         Gp = int(G + bb/2) / bb * bb;
         Bp = int(B + bb/2) / bb * bb;
         
         if (Rp > 255) Rp = 255;
         if (Gp > 255) Gp = 255;
         if (Bp > 255) Bp = 255;

         pylo = Brow * RES;                                                      //  range of pixels in block
         pyhi = pylo + RES;
         pxlo = Bcol * RES;
         pxhi = pxlo + RES;
         
         for (py = pylo; py < pyhi; py++)                                        //  loop pixels in block
         for (px = pxlo; px < pxhi; px++)
         {
            if (sa_stat == 3) {                                                  //  select area active
               ii = py * E3ww + px;
               dist = sa_pixmap[ii];                                             //  distance from edge
               if (! dist) continue;                                             //  pixel outside area
            }

            pix3 = PXMpix(E3pxm,px,py);                                          //  set pixels to
            pix3[0] = Rp;                                                        //    block palette color
            pix3[1] = Gp;
            pix3[2] = Bp;

            if (sa_stat == 3 && dist < sa_blendwidth)                            //  within area edge blend width
            {
               f1 = sa_blendfunc(dist);                                          //  blend changes over sa_blend
               f2 = 1.0 - f1;
               pix1 = PXMpix(E1pxm,px,py);
               pix3[0] = f1 * pix3[0] + f2 * pix1[0];
               pix3[1] = f1 * pix3[1] + f2 * pix1[1];
               pix3[2] = f1 * pix3[2] + f2 * pix1[2];
            }
         }

         Fbusy_done++;
      }

      zfree(blockR);
      zfree(blockG);
      zfree(blockB);

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

   return 0;                                                                     //  not executed, stop warning
}


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

//  Dither image using custom colors and Floyd-Steinberg error distribution algorithm.

namespace dither3_names
{
   editfunc    EFdither3;
   int         E3ww, E3hh;
   int         RES, RES2, COMP;
   char        palettename[100] = "none";                                        //  palette file name
   int         Ncolors = 0;                                                      //  palette color count
}


//  menu function

void m_dither3(GtkWidget *, const char *menu)                                    //  19.0
{
   using namespace dither3_names;

   int    dither3_dialog_event(zdialog* zd, const char *event);
   void * dither3_thread(void *);

   F1_help_topic = "dither";

   EFdither3.menuname = "dither3";
   EFdither3.menufunc = m_dither3;
   EFdither3.funcname = "dither3";                                               //  function name
   EFdither3.threadfunc = dither3_thread;                                        //  thread function
   EFdither3.Farea = 2;                                                          //  select area usable
   EFdither3.Frestart = 1;                                                       //  allow restart
   EFdither3.FusePL = 1;                                                         //  can use with paint/lever edits
   EFdither3.Fscript = 1;                                                        //  script file supported              20.0

   if (! edit_setup(EFdither3)) return;                                          //  setup edit
   
   E3ww = E3pxm->ww;
   E3hh = E3pxm->hh;

   if (Fscriptrun) Ncolors = 0;                                                  //  20.0

/***
       _________________________________
      |           Dither3               |
      |                                 |
      |  resolution  [ N ]              |                                        //  2x2 to 30x30
      |  palette: [palette name] [load] |                                        //  20.0
      |  [x] error compensation         |
      |                                 |
      |         [apply] [done] [cancel] |
      |_________________________________|
   
***/

   zdialog *zd = zdialog_new(E2X("Dither3"),Mwin,Bapply,Bdone,Bcancel,null);     //  dither dialog
   CEF->zd = zd;
   
   zdialog_add_widget(zd,"hbox","hbres","dialog");
   zdialog_add_widget(zd,"label","labres","hbres",E2X("resolution"),"space=5");
   zdialog_add_widget(zd,"zspin","RES","hbres","2|30|1|4","space=5|size=2");
   zdialog_add_widget(zd,"hbox","hbpal","dialog");
   zdialog_add_widget(zd,"hbox","hbpal","dialog");
   zdialog_add_widget(zd,"label","labpal","hbpal",E2X("palette:"),"space=5");
   zdialog_add_widget(zd,"text","palname","hbpal","none","space=5");
   zdialog_add_widget(zd,"button","LOAD","hbpal",Bload,"space=5");
   zdialog_add_widget(zd,"hbox","hbcomp","dialog");
   zdialog_add_widget(zd,"check","COMP","hbcomp",E2X("error compensation"),"space=5");

   if (! Fscriptrun) zdialog_restore_inputs(zd);                                 //  restore previous inputs
   zdialog_run(zd,dither3_dialog_event,"save");                                  //  run dialog - parallel

   return;
}


//  dither3 dialog event and completion function

int dither3_dialog_event(zdialog *zd, const char *event)
{
   using namespace dither3_names;

   int load_palette_map(cchar *file);

   char    palettefile[200], *pp;

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

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

         zdialog_fetch(zd,"RES",RES);
         zdialog_fetch(zd,"COMP",COMP);
         zdialog_fetch(zd,"palname",palettename,100);

         if (! Ncolors) {                                                        //  no palette loaded                  20.0
            snprintf(palettefile,200,"%s/%s",palettes_folder,palettename);
            Ncolors = load_palette_map(palettefile);                             //  CPU loops for a long time
         }

         if (Ncolors) signal_thread();                                           //  no palette loaded
      }

      else if (zd->zstat == 2) edit_done(0);                                     //  [done]
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   if (strmatch(event,"blendwidth")) signal_thread();
   
   if (strmatch(event,"LOAD"))                                                   //  load a palette file
   {
      pp = zgetfile(E2X("palette file"),MWIN,"file",palettes_folder,0);
      if (pp) {
         zdialog_show(zd,0);
         poptext_window(MWIN,"mapping palette ...",200,200,0,1000);
         zmainsleep(0.1);
         Ncolors = load_palette_map(pp);                                         //  CPU loops for a long time
         poptext_window(0,0,0,0,0,0);
         if (Ncolors) {
            zmessage_post(Mwin,"parent",3,"palette loaded, %d colors",Ncolors);
            pp = strrchr(pp,'/');
            zdialog_stuff(zd,"palname",pp+1);
         }
         zdialog_show(zd,1);
      }
   }
   
   return 1;
}


//  thread function - multiple working threads to update image

void * dither3_thread(void *)
{
   using namespace dither3_names;

   void match_palette_map(int r, int g, int b, int &R, int &G, int&B);

   int      Brows, Bcols, Brow, Bcol, block, Nblocks;
   int      pxlo, pxhi, pylo, pyhi, px, py;
   float    *blockR, *blockG, *blockB;
   float    R, G, B, Re, Ge, Be;
   int      Rp, Gp, Bp;
   int      n1, n2, n3, n4;
   int      ii, dist = 0;
   float    *pix1, *pix3;
   float    f1, f2;

   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request
      
      while (Fpaintrequest) zmainsleep(0.01); 
      paintlock(1);                                                              //  block window paint
      
      RES2 = RES * RES;                                                          //  pixel block size
      Bcols = E3ww / RES;                                                        //  block rows and cols
      Brows = E3hh / RES;

      Nblocks = Brows * Bcols;                                                   //  allocate block color memory
      blockR = (float *) zmalloc(Nblocks * sizeof(float));
      blockG = (float *) zmalloc(Nblocks * sizeof(float));
      blockB = (float *) zmalloc(Nblocks * sizeof(float));

      Fbusy_goal = Nblocks;
      Fbusy_done = 0;

      for (Brow = 0; Brow < Brows; Brow++)                                       //  loop blocks
      for (Bcol = 0; Bcol < Bcols; Bcol++)
      {
         block = Brow * Bcols + Bcol;                                            //  block sequence number

         pylo = Brow * RES;                                                      //  range of pixels in block
         pyhi = pylo + RES;
         pxlo = Bcol * RES;
         pxhi = pxlo + RES;
         
         R = G = B = 0;

         for (py = pylo; py < pyhi; py++)                                        //  loop pixels in block
         for (px = pxlo; px < pxhi; px++)
         {
            pix1 = PXMpix(E1pxm,px,py);                                          //  sum RGB for all pixels
            R += pix1[0];
            G += pix1[1];
            B += pix1[2];
         }
         
         blockR[block] = R / RES2;                                               //  save mean RGB values for block
         blockG[block] = G / RES2;
         blockB[block] = B / RES2;
      }

      if (COMP)
      {
         for (Brow = 1; Brow < Brows-1; Brow++)                                  //  loop blocks, omitting 
         for (Bcol = 1; Bcol < Bcols-1; Bcol++)                                  //    first/last rows/cols
         {
            block = Brow * Bcols + Bcol;                                         //  block sequence number
            
            R = blockR[block];                                                   //  block mean RGB
            G = blockG[block];
            B = blockB[block];
            
            if (R < 0) R = 0;
            if (R > 255) R = 255;
            if (G < 0) G = 0;
            if (G > 255) G = 255;
            if (B < 0) B = 0;
            if (B > 255) B = 255;
            
            match_palette_map(R,G,B,Rp,Gp,Bp);                                   //  get nearest palette RGB

            Re = R - Rp;                                                         //  errors
            Ge = G - Gp;
            Be = B - Bp;
            
            n1 = block + 1;                                                      //  distribute errors downstream
            n2 = block + Bcols - 1;
            n3 = n2 + 1;
            n4 = n3 + 1;
            
            blockR[n1] += 0.4375 * Re;
            blockG[n1] += 0.4375 * Ge;
            blockB[n1] += 0.4375 * Be;
            blockR[n2] += 0.1875 * Re;
            blockG[n2] += 0.1875 * Ge;
            blockB[n2] += 0.1875 * Be;
            blockR[n3] += 0.3125 * Re;
            blockG[n3] += 0.3125 * Ge;
            blockB[n3] += 0.3125 * Be;
            blockR[n4] += 0.0625 * Re;
            blockG[n4] += 0.0625 * Ge;
            blockB[n4] += 0.0625 * Be;
         }
      }

      for (Brow = 0; Brow < Brows; Brow++)                                       //  loop blocks
      for (Bcol = 0; Bcol < Bcols; Bcol++)
      {
         block = Brow * Bcols + Bcol;                                            //  block sequence number

         R = blockR[block];                                                      //  block mean RGB + errors
         G = blockG[block];
         B = blockB[block];
         
         if (R < 0) R = 0;
         if (R > 255) R = 255;
         if (G < 0) G = 0;
         if (G > 255) G = 255;
         if (B < 0) B = 0;
         if (B > 255) B = 255;

         match_palette_map(R,G,B,Rp,Gp,Bp);                                      //  get nearest palette RGB
         
         pylo = Brow * RES;                                                      //  range of pixels in block
         pyhi = pylo + RES;
         pxlo = Bcol * RES;
         pxhi = pxlo + RES;
         
         for (py = pylo; py < pyhi; py++)                                        //  loop pixels in block
         for (px = pxlo; px < pxhi; px++)
         {
            if (sa_stat == 3) {                                                  //  select area active
               ii = py * E3ww + px;
               dist = sa_pixmap[ii];                                             //  distance from edge
               if (! dist) continue;                                             //  pixel outside area
            }

            pix3 = PXMpix(E3pxm,px,py);                                          //  set pixels to
            pix3[0] = Rp;                                                        //    block palette color
            pix3[1] = Gp;
            pix3[2] = Bp;

            if (sa_stat == 3 && dist < sa_blendwidth)                            //  within area edge blend width
            {
               f1 = sa_blendfunc(dist);                                          //  blend changes over sa_blend
               f2 = 1.0 - f1;
               pix1 = PXMpix(E1pxm,px,py);
               pix3[0] = f1 * pix3[0] + f2 * pix1[0];
               pix3[1] = f1 * pix3[1] + f2 * pix1[1];
               pix3[2] = f1 * pix3[2] + f2 * pix1[2];
            }
         }

         Fbusy_done++;
      }

      zfree(blockR);
      zfree(blockG);
      zfree(blockB);

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

   return 0;                                                                     //  not executed, stop warning
}


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

namespace palette_map_names
{
   int      Np = 0, maxNp = 2000;
   int      palette[2000][3];
   int16    RGBmap[256][256][256];
}


//  Load a palette file and map all RGB colors to best palette color.
//  Returns palette color count, or 0 if error.

int load_palette_map(cchar *file)
{
   using namespace palette_map_names;
   
   void * load_palette_map_thread(void *arg);
   
   FILE     *fid;
   char     buffer[200];
   int      nn, R, G, B;
   
   Np = 0;

   printz("read palette file: %s \n",file);
   fid = fopen(file,"r");
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return 0;
   }

   while (true)
   {
      if (! fgets_trim(buffer,200,fid)) break;

      nn = sscanf(buffer,"%d %d %d",&R,&G,&B);
      if (nn != 3) continue;
      if (R < 0 || R > 255) continue;
      if (G < 0 || G > 255) continue;
      if (B < 0 || B > 255) continue;
      
      palette[Np][0] = R;
      palette[Np][1] = G;
      palette[Np][2] = B;
      Np++;
      if (Np == maxNp) break;
   }
   
   fclose(fid);
   
   if (Np < 6) {
      zmessageACK(Mwin,"palette has %d colors",Np);
      return 0;
   }
   
   if (Np == maxNp) {
      zmessageACK(Mwin,"palette exceeds %d color limit",maxNp);
      return 0;
   }
   
   do_wthreads(load_palette_map_thread,NWT);

   return Np;
}


//  thread function for mapping all RGB values to best-fit palette color

void * load_palette_map_thread(void *arg)
{
   using namespace palette_map_names;

   int      index = * ((int *) arg);
   int      Dr, Dg, Db, Dmin, D;
   int      R, G, B, p, Mp;

   for (R = index; R < 256; R += NWT)
   for (G = 0; G < 256; G++)
   for (B = 0; B < 256; B++)
   {
      Dmin = 99999999;
      Mp = 0;

      for (p = 0; p < Np; p++)
      {
         Dr = R - palette[p][0];
         Dg = G - palette[p][1];
         Db = B - palette[p][2];
         D = Dr * Dr + Dg * Dg + Db * Db;
         if (D > Dmin) continue;
         Dmin = D;
         Mp = p;
      }
      RGBmap[R][G][B] = Mp;
   }

   pthread_exit(0);
}


//  return the best matching palette RGB for a given input RGB

void match_palette_map(int r, int g, int b, int &R, int &G, int&B)
{
   using namespace palette_map_names;
   
   int   p;
   
   R = G = B = 0;
   if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) return;
   if (! Np) return;

   p = RGBmap[r][g][b];
   R = palette[p][0];
   G = palette[p][1];
   B = palette[p][2];
}


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

//  convert image to simulate a painting

namespace painting_names
{
   editfunc    EFpainting;

   int         color_depth;
   int         group_area;
   float       color_match;
   int         borders;

   typedef struct  {
      int16       px, py;
      char        direc;
   }  spixstack;

   int         Nstack;
   spixstack   *pixstack;                                                        //  pixel group search memory
   int         *pixgroup;                                                        //  maps (px,py) to pixel group no.
   int         *groupcount;                                                      //  count of pixels in each group

   int         group;
   char        direc;
   float       gcolor[3];
}


void m_painting(GtkWidget *, cchar *menu)
{
   using namespace painting_names;

   int    painting_dialog_event(zdialog *zd, cchar *event);
   void * painting_thread(void *);

   F1_help_topic = "painting";

   EFpainting.menuname = menu;
   EFpainting.menufunc = m_painting;
   EFpainting.funcname = "painting";
   EFpainting.Farea = 2;                                                         //  select area usable
   EFpainting.Fscript = 1;                                                       //  scripting supported
   EFpainting.threadfunc = painting_thread;                                      //  thread function
   if (! edit_setup(EFpainting)) return;                                         //  setup edit

   zdialog *zd = zdialog_new(E2X("Painting"),Mwin,Bapply,Bdone,Bcancel,null);
   EFpainting.zd = zd;

/***
       _____________________________
      |    Simulate Painting        |
      |                             |
      |  color depth [____]         |
      |  patch area goal [____]     |
      |  req. color match [____]    |
      |  borders [_]                |
      |                             |
      |     [Apply] [Done] [Cancel] |
      |_____________________________|
      
***/

   zdialog_add_widget(zd,"hbox","hbcd","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","lab1","hbcd",E2X("color depth"),"space=5");
   zdialog_add_widget(zd,"zspin","colordepth","hbcd","1|5|1|3","space=5");

   zdialog_add_widget(zd,"hbox","hbts","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labts","hbts",E2X("patch area goal"),"space=5");
   zdialog_add_widget(zd,"zspin","grouparea","hbts","0|9999|10|1000","space=5");

   zdialog_add_widget(zd,"hbox","hbcm","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labcm","hbcm",E2X("req. color match"),"space=5");
   zdialog_add_widget(zd,"zspin","colormatch","hbcm","0|99|1|50","space=5");

   zdialog_add_widget(zd,"hbox","hbbd","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labbd","hbbd",E2X("borders"),"space=5");
   zdialog_add_widget(zd,"check","borders","hbbd",0,"space=2");

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


//  dialog event and completion callback function

int painting_dialog_event(zdialog *zd, cchar *event)
{
   using namespace painting_names;

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

   if (zd->zstat)
   {  
      if (zd->zstat == 1) {                                                      //  apply
         zd->zstat = 0;                                                          //  keep dialog active
         zdialog_fetch(zd,"colordepth",color_depth);                             //  color depth
         zdialog_fetch(zd,"grouparea",group_area);                               //  target group area (pixels)
         zdialog_fetch(zd,"colormatch",color_match);                             //  req. color match to combine groups
         zdialog_fetch(zd,"borders",borders);                                    //  borders wanted
         color_match = 0.01 * color_match;                                       //  scale 0 to 1
         signal_thread();                                                        //  do the work
      }
      else if (zd->zstat == 2) edit_done(0);                                     //  done, commit edit
      else edit_cancel(0);                                                       //  cancel, discard edit
      return 1;
   }
   
   return 0;
}


//  painting thread function

void * painting_thread(void *)
{
   void  painting_colordepth();
   void  painting_pixgroups();
   void  painting_mergegroups();
   void  painting_paintborders();
   void  painting_blend();

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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      painting_colordepth();                                                     //  set new color depth
      painting_pixgroups();                                                      //  group pixel patches of a color
      painting_mergegroups();                                                    //  merge smaller into larger groups
      painting_paintborders();                                                   //  add borders around groups
      painting_blend();                                                          //  blend edges of selected area

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

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

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


//  set the specified color depth, 1-5 bits/color

void painting_colordepth()
{
   using namespace painting_names;

   int            ii, px, py, rgb;
   uint16         m1, m2, val1, val3;
   float          fmag, *pix1, *pix3;

   m1 = 0xFFFF << (16 - color_depth);                                            //  5 > 1111100000000000
   m2 = 0x8000 >> color_depth;                                                   //  5 > 0000010000000000

   fmag = 65535.0 / m1;                                                          //  full brightness range

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

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

      for (rgb = 0; rgb < 3; rgb++)
      {
         val1 = 256 * pix1[rgb];
         if (val1 < m1) val3 = (val1 + m2) & m1;
         else val3 = m1;
         val3 = val3 * fmag;
         pix3[rgb] = val3 / 256;
      }
   }

   return;
}


//  find all groups of contiguous pixels with the same color

void painting_pixgroups()
{
   using namespace painting_names;

   void  painting_pushpix(int px, int py);

   int            cc1, cc2;
   int            ii, kk, px, py;
   int            ppx, ppy, npx, npy;
   float          *pix3;

   cc1 = E3pxm->ww * E3pxm->hh;

   cc2 = cc1 * sizeof(int);
   pixgroup = (int *) zmalloc(cc2);                                              //  maps pixel to assigned group
   memset(pixgroup,0,cc2);

   if (sa_stat == 3) cc1 = sa_Npixel;

   cc2 = cc1 * sizeof(spixstack);
   pixstack = (spixstack *) zmalloc(cc2);                                        //  memory stack for pixel search
   memset(pixstack,0,cc2);

   cc2 = cc1 * sizeof(int);
   groupcount = (int *) zmalloc(cc2);                                            //  counts pixels per group
   memset(groupcount,0,cc2);

   group = 0;

   for (py = 0; py < E3pxm->hh; py++)                                            //  loop all pixels
   for (px = 0; px < E3pxm->ww; px++)
   {
      kk = py * E3pxm->ww + px;
      if (sa_stat == 3 && ! sa_pixmap[kk]) continue;
      if (pixgroup[kk]) continue;                                                //  already assigned to group

      pixgroup[kk] = ++group;                                                    //  assign next group
      ++groupcount[group];

      pix3 = PXMpix(E3pxm,px,py);
      gcolor[0] = pix3[0];
      gcolor[1] = pix3[1];
      gcolor[2] = pix3[2];

      pixstack[0].px = px;                                                       //  put pixel into stack with
      pixstack[0].py = py;                                                       //    direction = ahead
      pixstack[0].direc = 'a';
      Nstack = 1;

      while (Nstack)                                                             //  overhauled
      {
         kk = Nstack - 1;                                                        //  get last pixel in stack
         px = pixstack[kk].px;
         py = pixstack[kk].py;
         direc = pixstack[kk].direc;

         if (direc == 'x') {
            Nstack--;
            continue;
         }

         if (Nstack > 1) {
            ii = Nstack - 2;                                                     //  get prior pixel in stack
            ppx = pixstack[ii].px;
            ppy = pixstack[ii].py;
         }
         else {
            ppx = px - 1;                                                        //  if only one, assume prior = left
            ppy = py;
         }

         if (direc == 'a') {                                                     //  next ahead pixel
            npx = px + px - ppx;
            npy = py + py - ppy;
            pixstack[kk].direc = 'r';                                            //  next search direction
         }

         else if (direc == 'r') {                                                //  next right pixel
            npx = px + py - ppy;
            npy = py + px - ppx;
            pixstack[kk].direc = 'l';
         }

         else { /*  direc = 'l'  */                                              //  next left pixel
            npx = px + ppy - py;
            npy = py + ppx - px;
            pixstack[kk].direc = 'x';
         }

         if (npx < 0 || npx > E3pxm->ww-1) continue;                             //  pixel off the edge
         if (npy < 0 || npy > E3pxm->hh-1) continue;

         kk = npy * E3pxm->ww + npx;

         if (sa_stat == 3 && ! sa_pixmap[kk]) continue;                          //  pixel outside area

         if (pixgroup[kk]) continue;                                             //  pixel already assigned

         pix3 = PXMpix(E3pxm,npx,npy);
         if (pix3[0] != gcolor[0] || pix3[1] != gcolor[1]                        //  not same color as group
                                  || pix3[2] != gcolor[2]) continue;

         pixgroup[kk] = group;                                                   //  assign pixel to group
         ++groupcount[group];

         kk = Nstack++;                                                          //  put pixel into stack
         pixstack[kk].px = npx;
         pixstack[kk].py = npy;
         pixstack[kk].direc = 'a';                                               //  direction = ahead
      }
   }

   return;
}


//  merge small pixel groups into adjacent larger groups with best color match

void painting_mergegroups()
{
   using namespace painting_names;

   int         ii, jj, kk, px, py, npx, npy;
   int         nccc, mcount, group2;
   int         nnpx[4] = {  0, -1, +1, 0 };
   int         nnpy[4] = { -1, 0,  0, +1 };
   float       match;
   float       *pix3, *pixN;

   typedef struct  {
      int         group;
      double      match;
      float       pixM[3];
   }  snewgroup;

   snewgroup      *newgroup;

   nccc = (group + 1) * sizeof(snewgroup);
   newgroup = (snewgroup *) zmalloc(nccc);

   if (sa_stat == 3)                                                             //  process select area
   {
      while (true)
      {
         memset(newgroup,0,nccc);

         for (ii = 0; ii < E3pxm->ww * E3pxm->hh; ii++)                          //  find pixels in select area
         {
            if (! sa_pixmap[ii]) continue;
            py = ii / E3pxm->ww;
            px = ii - py * E3pxm->ww;

            kk = E3pxm->ww * py + px;                                            //  get assigned group
            group = pixgroup[kk];
            if (groupcount[group] >= group_area) continue;                       //  group count large enough

            pix3 = PXMpix(E3pxm,px,py);

            for (jj = 0; jj < 4; jj++)                                           //  get 4 neighbor pixels
            {
               npx = px + nnpx[jj];
               npy = py + nnpy[jj];

               if (npx < 0 || npx >= E3pxm->ww) continue;                        //  off the edge
               if (npy < 0 || npy >= E3pxm->hh) continue;

               kk = E3pxm->ww * npy + npx;
               if (! sa_pixmap[kk]) continue;                                    //  pixel outside area
               if (pixgroup[kk] == group) continue;                              //  already in same group

               pixN = PXMpix(E3pxm,npx,npy);                                     //  match group neighbor color to my color
               match = PIXMATCH(pix3,pixN);                                      //  0..1 = zero..perfect match

               if (match < color_match) continue;

               if (match > newgroup[group].match) {
                  newgroup[group].match = match;                                 //  remember best match
                  newgroup[group].group = pixgroup[kk];                          //  and corresp. group no.
                  newgroup[group].pixM[0] = pixN[0];                             //  and corresp. new color
                  newgroup[group].pixM[1] = pixN[1];
                  newgroup[group].pixM[2] = pixN[2];
               }
            }
         }

         mcount = 0;

         for (ii = 0; ii < E3pxm->ww * E3pxm->hh; ii++)                          //  find pixels in select area
         {
            if (! sa_pixmap[ii]) continue;
            py = ii / E3pxm->ww;
            px = ii - py * E3pxm->ww;

            kk = E3pxm->ww * py + px;
            group = pixgroup[kk];                                                //  test for new group assignment
            group2 = newgroup[group].group;
            if (! group2) continue;

            if (groupcount[group] > groupcount[group2]) continue;                //  accept only bigger new group

            pixgroup[kk] = group2;                                               //  make new group assignment
            --groupcount[group];
            ++groupcount[group2];

            pix3 = PXMpix(E3pxm,px,py);                                          //  make new color assignment
            pix3[0] = newgroup[group].pixM[0];
            pix3[1] = newgroup[group].pixM[1];
            pix3[2] = newgroup[group].pixM[2];

            mcount++;
         }

         if (mcount == 0) break;
      }
   }

   else                                                                          //  process entire image
   {
      while (true)
      {
         memset(newgroup,0,nccc);

         for (py = 0; py < E3pxm->hh; py++)                                      //  loop all pixels
         for (px = 0; px < E3pxm->ww; px++)
         {
            kk = E3pxm->ww * py + px;                                            //  get assigned group
            group = pixgroup[kk];
            if (groupcount[group] >= group_area) continue;                       //  group count large enough

            pix3 = PXMpix(E3pxm,px,py);

            for (jj = 0; jj < 4; jj++)                                           //  get 4 neighbor pixels
            {
               npx = px + nnpx[jj];
               npy = py + nnpy[jj];

               if (npx < 0 || npx >= E3pxm->ww) continue;                        //  off the edge
               if (npy < 0 || npy >= E3pxm->hh) continue;

               kk = E3pxm->ww * npy + npx;
               if (pixgroup[kk] == group) continue;                              //  in same group

               pixN = PXMpix(E3pxm,npx,npy);                                     //  match color of group neighbor
               match = PIXMATCH(pix3,pixN);                                      //  0..1 = zero..perfect match

               if (match < color_match) continue;

               if (match > newgroup[group].match) {
                  newgroup[group].match = match;                                 //  remember best match
                  newgroup[group].group = pixgroup[kk];                          //  and corresp. group no.
                  newgroup[group].pixM[0] = pixN[0];                             //  and corresp. new color
                  newgroup[group].pixM[1] = pixN[1];
                  newgroup[group].pixM[2] = pixN[2];
               }
            }
         }

         mcount = 0;

         for (py = 0; py < E3pxm->hh; py++)                                      //  loop all pixels
         for (px = 0; px < E3pxm->ww; px++)
         {
            kk = E3pxm->ww * py + px;
            group = pixgroup[kk];                                                //  test for new group assignment
            group2 = newgroup[group].group;
            if (! group2) continue;

            if (groupcount[group] > groupcount[group2]) continue;                //  accept only bigger new group

            pixgroup[kk] = group2;                                               //  make new group assignment
            --groupcount[group];
            ++groupcount[group2];

            pix3 = PXMpix(E3pxm,px,py);                                          //  make new color assignment
            pix3[0] = newgroup[group].pixM[0];
            pix3[1] = newgroup[group].pixM[1];
            pix3[2] = newgroup[group].pixM[2];

            mcount++;
         }

         if (mcount == 0) break;
      }
   }

   zfree(pixgroup);
   zfree(pixstack);
   zfree(groupcount);
   zfree(newgroup);

   return;
}


//  paint borders between the groups of contiguous pixels

void painting_paintborders()
{
   using namespace painting_names;

   int            ii, kk, px, py, cc;
   float          *pix3, *pixL, *pixA;

   if (! borders) return;

   cc = E3pxm->ww * E3pxm->hh;
   char * pixblack = (char *) zmalloc(cc);
   memset(pixblack,0,cc);

   if (sa_stat == 3)
   {
      for (ii = 0; ii < E3pxm->ww * E3pxm->hh; ii++)                             //  find pixels in select area
      {
         if (! sa_pixmap[ii]) continue;
         py = ii / E3pxm->ww;
         px = ii - py * E3pxm->ww;
         if (px < 1 || py < 1) continue;

         pix3 = PXMpix(E3pxm,px,py);
         pixL = PXMpix(E3pxm,px-1,py);
         pixA = PXMpix(E3pxm,px,py-1);

         if (pix3[0] != pixL[0] || pix3[1] != pixL[1] || pix3[2] != pixL[2])
         {
            kk = ii - 1;
            if (pixblack[kk]) continue;
            kk += 1;
            pixblack[kk] = 1;
            continue;
         }

         if (pix3[0] != pixA[0] || pix3[1] != pixA[1] || pix3[2] != pixA[2])
         {
            kk = ii - E3pxm->ww;
            if (pixblack[kk]) continue;
            kk += E3pxm->ww;
            pixblack[kk] = 1;
         }
      }

      for (ii = 0; ii < E3pxm->ww * E3pxm->hh; ii++)                             //  find pixels in select area
      {
         if (! sa_pixmap[ii]) continue;
         py = ii / E3pxm->ww;
         px = ii - py * E3pxm->ww;
         if (px < 1 || py < 1) continue;

         if (! pixblack[ii]) continue;
         pix3 = PXMpix(E3pxm,px,py);
         pix3[0] = pix3[1] = pix3[2] = 0;
      }
   }

   else
   {
      for (py = 1; py < E3pxm->hh; py++)                                         //  loop all pixels
      for (px = 1; px < E3pxm->ww; px++)                                         //  omit top and left
      {
         pix3 = PXMpix(E3pxm,px,py);                                             //  output pixel
         pixL = PXMpix(E3pxm,px-1,py);                                           //  pixel to left
         pixA = PXMpix(E3pxm,px,py-1);                                           //  pixel above

         if (pix3[0] != pixL[0] || pix3[1] != pixL[1] || pix3[2] != pixL[2])
         {
            kk = E3pxm->ww * py + px-1;                                          //  have horiz. transition
            if (pixblack[kk]) continue;
            kk += 1;
            pixblack[kk] = 1;
            continue;
         }

         if (pix3[0] != pixA[0] || pix3[1] != pixA[1] || pix3[2] != pixA[2])
         {
            kk = E3pxm->ww * (py-1) + px;                                        //  have vertical transition
            if (pixblack[kk]) continue;
            kk += E3pxm->ww;
            pixblack[kk] = 1;
         }
      }

      for (py = 1; py < E3pxm->hh; py++)
      for (px = 1; px < E3pxm->ww; px++)
      {
         kk = E3pxm->ww * py + px;
         if (! pixblack[kk]) continue;
         pix3 = PXMpix(E3pxm,px,py);
         pix3[0] = pix3[1] = pix3[2] = 0;
      }
   }

   zfree(pixblack);
   return;
}


//  blend edges of selected area

void painting_blend()
{
   int         ii, px, py, dist;
   float       *pix1, *pix3;
   double      f1, f2;

   if (sa_stat == 3 && sa_blendwidth > 0)
   {
      for (ii = 0; ii < E3pxm->ww * E3pxm->hh; ii++)                             //  find pixels in select area
      {
         dist = sa_pixmap[ii];
         if (! dist || dist >= sa_blendwidth) continue;

         py = ii / E3pxm->ww;
         px = ii - py * E3pxm->ww;
         pix1 = PXMpix(E1pxm,px,py);                                             //  input pixel
         pix3 = PXMpix(E3pxm,px,py);                                             //  output pixel

         f2 = sa_blendfunc(dist);                                                //  changes over distance sa_blendwidth
         f1 = 1.0 - f2;

         pix3[0] = f1 * pix1[0] + f2 * pix3[0];
         pix3[1] = f1 * pix1[1] + f2 * pix3[1];
         pix3[2] = f1 * pix1[2] + f2 * pix3[2];
      }
   }

   return;
}


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

//  texture menu function
//  apply a random texture to the image

namespace texturenames
{
   editfunc    EFtexture;                                                        //  edit function data
   int         radius = 4;
   int         power = 40;
   int         e3ww, e3hh;
}

void m_texture(GtkWidget *, cchar *menu) 
{
   using namespace texturenames;

   int    texture_dialog_event(zdialog* zd, const char *event);
   void * texture_thread(void *);

   zdialog  *zd;

   F1_help_topic = "texture";

   EFtexture.menuname = menu;
   EFtexture.menufunc = m_texture;
   EFtexture.funcname = "texture";                                               //  function name
   EFtexture.Farea = 2;                                                          //  select area OK
   EFtexture.Fscript = 1;                                                        //  scripting supported
   EFtexture.threadfunc = texture_thread;                                        //  thread function

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

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

/***
       ___________________________________
      |          Add Texture              |
      |                                   |
      |  Radius [___]  Power [___]        |
      |                                   |
      |          [apply] [done] [cancel]  |
      |___________________________________|

***/

   zd = zdialog_new(E2X("Add Texture"),Mwin,Bapply,Bdone,Bcancel,null);          //  texture dialog
   CEF->zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10");
   zdialog_add_widget(zd,"label","labrad","hb1",Bradius,"space=3");
   zdialog_add_widget(zd,"zspin","radius","hb1","1|40|1|4");
   zdialog_add_widget(zd,"label","space","hb1",0,"space=1");
   zdialog_add_widget(zd,"label","labpow","hb1",Bpower);
   zdialog_add_widget(zd,"zspin","power","hb1","1|100|1|40","space=3");

   zdialog_stuff(zd,"radius",radius);
   zdialog_stuff(zd,"power",power);

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


//  texture dialog event and completion function

int texture_dialog_event(zdialog *zd, const char *event)
{
   using namespace texturenames;

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  apply
         zd->zstat = 0;                                                          //  keep dialog active
         edit_reset();
         zdialog_fetch(zd,"radius",radius);
         zdialog_fetch(zd,"power",power);
         signal_thread();                                                        //  process
         return 1;
      }
      if (zd->zstat == 2) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   return 1;
}


//  thread function - update image

void * texture_thread(void *)
{
   using namespace texturenames;

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

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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      if (sa_stat == 3) Fbusy_goal = sa_Npixel;                                  //  set up progress monitor
      else  Fbusy_goal = e3ww * e3hh;
      Fbusy_done = 0;

      do_wthreads(texture_wthread,NWT);                                          //  worker threads

      Fbusy_goal = Fbusy_done = 0;
      CEF->Fmods = 1;                                                            //  image modified
      CEF->Fsaved = 0;                                                           //  not saved

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop warning
}


void * texture_wthread(void *arg)                                                //  worker thread function
{
   using namespace texturenames;

   int         index = *((int *) (arg));
   int         px, py, qx, qy, npix, dist = 0;
   int         ii, rank1, rank2;
   float       fred, fgreen, fblue;
   float       *pix1, *pix3, *qpix;
   float       pow, ff, f1, f3;
   float       pbright, qbright;

   pow = 0.001 * powf(power,1.5);                                                //  0.001 ... 1.0

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

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

      npix = 0;
      rank1 = rank2 = 0;

      for (qy = py-radius; qy <= py+radius; qy++)                                //  loop pixels in neighborhood
      for (qx = px-radius; qx <= px+radius; qx++)
      {
         qpix = PXMpix(E1pxm,qx,qy);                                             //  neighbor pixel
         qbright = pixbright(qpix);                                              //  compare neighbor brightness
         if (qbright < pbright) rank1++;                                         //  count darker neighbors
         if (qbright == pbright) rank2++;                                        //  count equal neighbors
         npix++;
      }

      if (! pbright) ff = 1;
      else {
         qbright = 255.9 * (rank1 + 0.5 * rank2) / npix;                         //  assigned brightness = 256*rank/npix
         ff = qbright / pbright;                                                 //  new/old brightness ratio
      }

      ff = 1.0 - pow * (1.0 - ff);                                               //  1 ... ff  for pow = 0 ... 1

      fred = ff * pix1[0];                                                       //  RGB for new brightness level
      fgreen = ff * pix1[1];
      fblue = ff * pix1[2];

      if (fred > 255.9) fred = 255.9;                                            //  stop overflow
      if (fgreen > 255.9) fgreen = 255.9;
      if (fblue > 255.9) fblue = 255.9;

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         f3 = sa_blendfunc(dist);                                                //    blend changes over sa_blendwidth
         f1 = 1.0 - f3;
         fred = f3 * fred + f1 * pix1[0];
         fgreen = f3 * fgreen + f1 * pix1[1];
         fblue = f3 * fblue + f1 * pix1[2];
      }

      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      pix3[0] = fred;
      pix3[1] = fgreen;
      pix3[2] = fblue;

      Fbusy_done++;
   }

   pthread_exit(0);                                                              //  exit thread
}


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

//  pattern menu function
//  tile the image with a repeating pattern

namespace patternnames
{
   editfunc    EFpattern;                                                        //  edit function data
   int         e3ww, e3hh;
   char        *pattfile = 0;                                                    //  pattern image file
   PIXBUF      *pixbuf = 0;                                                      //  pattern pixbuf
   int         pattww, patthh, pattrs;                                           //  pixbuf length, width, row stride
   uint8       *pixels;                                                          //  pixbuf pixels
   int         olapww = 0, olaphh = 0;                                           //  pattern overlap
   int         opacity = 0;                                                      //  pattern opacity 0-100
   int         contrast = 100;                                                   //  pattern contrast 0-200
   float       zoom = 1.0;                                                       //  pattern magnification
   int         Fgray = 0;                                                        //  grayscale = no
}


//  menu function

void m_pattern(GtkWidget *, cchar *menu)
{
   using namespace patternnames;

   int   pattern_dialog_event(zdialog* zd, const char *event);

   zdialog  *zd;

   F1_help_topic = "pattern";

   EFpattern.menuname = menu;
   EFpattern.menufunc = m_pattern;
   EFpattern.funcname = "pattern";                                               //  function name
   EFpattern.Farea = 2;                                                          //  select area OK
   EFpattern.Fscript = 1;                                                        //  scripting supported

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

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

/***
       _______________________________________________
      | [x][-][_]  Background Pattern                 |
      |                                               |
      |  Pattern File [____________________] [Browse] |
      |  Zoom [___]  Geometry [Calculate]             |
      |  Pattern Width NNN   Height NNN               |                          //  labels not spin buttons 
      |  Overlap Width [___]  Height [___]            |
      |  Opacity [___]  Contrast [___]  [x] Grayscale |
      |                                               |
      |                       [apply] [done] [cancel] |
      |_______________________________________________|

***/

   zd = zdialog_new(E2X("Background Pattern"),Mwin,Bapply,Bdone,Bcancel,null);
   CEF->zd = zd;

   zdialog_add_widget(zd,"hbox","hbfile","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labfile","hbfile",E2X("Pattern File:"),"space=5");
   zdialog_add_widget(zd,"zentry","pattfile","hbfile",0,"expand");
   zdialog_add_widget(zd,"button","browse","hbfile",Bbrowse,"space=5");

   zdialog_add_widget(zd,"hbox","hbcalc","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labzoom","hbcalc",E2X("Zoom"),"space=5");
   zdialog_add_widget(zd,"zspin","zoom","hbcalc","1.0|5.0|0.01|1.0");
   zdialog_add_widget(zd,"label","space","hbcalc",0,"space=10");
   zdialog_add_widget(zd,"label","labcalc","hbcalc",E2X("Geometry"),"space=5");
   zdialog_add_widget(zd,"button","calc","hbcalc",Bcalculate);

   zdialog_add_widget(zd,"hbox","hbs11","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labpatt","hbs11",E2X("Pattern"),"space=3");
   zdialog_add_widget(zd,"label","labwidth","hbs11",Bwidth,"space=3");
   zdialog_add_widget(zd,"label","pattww","hbs11","tbd","space=5");
   zdialog_add_widget(zd,"label","space","hbs11",0,"space=10");
   zdialog_add_widget(zd,"label","labheight","hbs11",Bheight,"space=3");
   zdialog_add_widget(zd,"label","patthh","hbs11","tbd","space=5");

   zdialog_add_widget(zd,"hbox","hbs12","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labover","hbs12",E2X("Overlap"),"space=3");
   zdialog_add_widget(zd,"label","labwidth","hbs12",Bwidth,"space=3");
   zdialog_add_widget(zd,"zspin","olapww","hbs12","0|1000|1|0");
   zdialog_add_widget(zd,"label","space","hbs12",0,"space=10");
   zdialog_add_widget(zd,"label","labheight","hbs12",Bheight,"space=3");
   zdialog_add_widget(zd,"zspin","olaphh","hbs12","0|1000|1|0");

   zdialog_add_widget(zd,"hbox","hbopac","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labopac","hbopac",E2X("Opacity"),"space=5");
   zdialog_add_widget(zd,"zspin","opacity","hbopac","0|100|1|0");
   zdialog_add_widget(zd,"label","space","hbopac",0,"space=10");
   zdialog_add_widget(zd,"label","labcont","hbopac",E2X("Contrast"),"space=5");
   zdialog_add_widget(zd,"zspin","contrast","hbopac","0|200|1|100");
   zdialog_add_widget(zd,"label","space","hbopac",0,"space=10");
   zdialog_add_widget(zd,"check","gray","hbopac",E2X("Grayscale"));

   zdialog_restore_inputs(zd);                                                   //  restore previous inputs
   zdialog_run(zd,pattern_dialog_event,"save");                                  //  run dialog - parallel

   zdialog_send_event(zd,"init");                                                //  initialize
   return;
}


//  pattern dialog event and completion function

int pattern_dialog_event(zdialog *zd, const char *event)                         //  overhauled to retain prior data
{
   using namespace patternnames;

   void  pattern_match();
   void  pattern_image();

   char        temp1[150], temp2[200];
   char        *file, *pp;
   PIXBUF      *pixbuf2;
   GError      *gerror = 0;

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  apply                              20.0
         zd->zstat = 0;                                                          //  keep dialog alive
         pattern_image();                                                        //  calculate image + pattern
         CEF->Fmods = 1;                                                         //  image modified
         CEF->Fsaved = 0;                                                        //  not saved
         Fpaint2();                                                              //  update window
         return 1;
      }

      else {
         if (zd->zstat == 2) edit_done(0);                                       //  commit edit
         else edit_cancel(0);                                                    //  discard edit
         if (pixbuf) g_object_unref(pixbuf);                                     //  free memory
         pixbuf = 0;
         if (pattfile) zfree(pattfile);
         pattfile = 0;
         return 1;
      }
   }
   
   zdialog_fetch(zd,"olapww",olapww);
   zdialog_fetch(zd,"olaphh",olaphh);
   zdialog_fetch(zd,"zoom",zoom);
   zdialog_fetch(zd,"opacity",opacity);
   zdialog_fetch(zd,"contrast",contrast);

   if (strmatch(event,"init")) 
   {
      if (pattfile) zfree(pattfile);
      pattfile = 0;
      if (pixbuf) g_object_unref(pixbuf);
      pixbuf = 0;
      zdialog_fetch(zd,"pattfile",temp1,150);                                    //  get prior pattern file
      if (*temp1 > ' ') {
         snprintf(temp2,200,"%s/%s",pattern_folder,temp1);
         pattfile = zstrdup(temp2);
      }
      
      zdialog_stuff(zd,"gray",0);                                                //  grayscale off
      Fgray = 0;
   }
   
   if (strmatch(event,"gray"))                                                   //  20.0
      zdialog_fetch(zd,"gray",Fgray);

   if (strmatch(event,"browse"))                                                 //  choose new pattern file
   {
      if (pattfile) pp = pattfile;
      else pp = pattern_folder;
      file = gallery_select1(pp);                                                //  use thumbnail selection
      if (! file) return 1;
      if (pattfile) zfree(pattfile);
      pattfile = file;
      pp = strrchr(pattfile,'/');                                                //  update dialog
      if (pp) zdialog_stuff(zd,"pattfile",pp+1);
      olapww = olaphh = 0;                                                       //  reset zoom, overlaps
      zoom = 1;
   }

   if (zstrstr("init browse",event))                                             //  open pattern file
   {
      if (! pattfile) return 1;
      if (pixbuf) g_object_unref(pixbuf);
      pixbuf = gdk_pixbuf_new_from_file(pattfile,&gerror);                       //  create pixbuf image
      if (! pixbuf) {
         zmessageACK(Mwin,"bad pattern file: %s",pattfile);                      //  not an image file
         zfree(pattfile);
         pattfile = 0;
         return 1;
      }
      
      if (! strstr(pattfile,pattern_folder))                                     //  add pattern to folder if needed
         copyFile(pattfile,pattern_folder);

      pixbuf2 = gdk_pixbuf_stripalpha(pixbuf);                                   //  strip alpha channel if present
      if (pixbuf2) {
         g_object_unref(pixbuf);
         pixbuf = pixbuf2;
      }

      pattrs = gdk_pixbuf_get_rowstride(pixbuf);                                 //  row stride
      pixels = gdk_pixbuf_get_pixels(pixbuf);                                    //  image data (pixels)
   }
   
   if (! pixbuf) return 1;                                                       //  continuation pointless

   pattww = gdk_pixbuf_get_width(pixbuf);                                        //  pixbuf dimensions
   patthh = gdk_pixbuf_get_height(pixbuf);

   if (strmatch(event,"calc")) {                                                 //  find pattern dimensions automatically
      pattern_match();
      olapww = olaphh = 0;                                                       //  reset overlaps
   }

   if (zstrstr("zoom init",event))                                               //  set pattern zoom level
   {
      g_object_unref(pixbuf);
      pixbuf = gdk_pixbuf_new_from_file(pattfile,&gerror);                       //  refresh pixbuf at scale 1x
      if (! pixbuf) {
         zmessageACK(Mwin,"%s \n %s",pattfile,gerror->message);
         return 1;
      }

      pixbuf2 = gdk_pixbuf_stripalpha(pixbuf);                                   //  strip alpha channel if present 
      if (pixbuf2) {
         g_object_unref(pixbuf);
         pixbuf = pixbuf2;
      }

      pattww = gdk_pixbuf_get_width(pixbuf);                                     //  pixbuf dimensions
      patthh = gdk_pixbuf_get_height(pixbuf);

      pattww = pattww * zoom;                                                    //  new dimensions
      patthh = patthh * zoom;

      pixbuf2 = gdk_pixbuf_scale_simple(pixbuf,pattww,patthh,BILINEAR);
      g_object_unref(pixbuf);                                                    //  replace original pixbuf
      pixbuf = pixbuf2;
      if (! pixbuf) return 1;
      
      pattww = gdk_pixbuf_get_width(pixbuf);                                     //  new pixbuf dimensions
      patthh = gdk_pixbuf_get_height(pixbuf);
      pattrs = gdk_pixbuf_get_rowstride(pixbuf);                                 //  row stride
      pixels = gdk_pixbuf_get_pixels(pixbuf);                                    //  image data (pixels)
   }
   
   if (olapww > pattww/3) olapww = pattww/3;                                     //  prevent nonsense 
   if (olaphh > patthh/3) olaphh = patthh/3;
   
   zdialog_stuff(zd,"pattww",pattww);                                            //  stuff all dialog parameters
   zdialog_stuff(zd,"patthh",patthh);
   zdialog_stuff(zd,"olapww",olapww);
   zdialog_stuff(zd,"olaphh",olaphh);
   zdialog_stuff(zd,"zoom",zoom);
   zdialog_stuff(zd,"opacity",opacity);
   zdialog_stuff(zd,"contrast",contrast);

   return 1;
}


//  set tile dimensions to match the pitch of a repeating pattern

void pattern_match()
{
   using namespace patternnames;

   int      limx, limy;
   int      px, py, qx, qy, sx, sy;
   int      diff, mindiff;
   uint8    *pix1, *pix2;

   limx = pattww / 2;
   limy = patthh / 2;

   if (limx < 21 || limy < 21) return;

   sx = sy = 1;                                                                  //  best origin found
   mindiff = 999999999;                                                          //  best pixel difference found

   for (px = 20; px < limx; px++)                                                //  loop all possible pattern origins
   for (py = 20; py < limy; py++)                                                //  (pattern must be > 20 x 20)
   {
      diff = 0;

      for (qy = py; qy < py + limy; qy++)                                        //  match vert. lines from (1,1) and (px,py)
      {
         pix1 = pixels + (1+qy-py) * pattrs + 1 * 3;                             //  line from (1,1)
         pix2 = pixels + qy * pattrs + px * 3;                                   //  line from (qx,qy)
         diff += abs(pix1[0] - pix2[0])
               + abs(pix1[1] - pix2[1])
               + abs(pix1[2] - pix2[2]);
      }

      for (qx = px; qx < px + limx; qx++)                                        //  match horz. lines from (1,1) and (px,py)
      {
         pix1 = pixels + 1 * pattrs + (1+qx-px) * 3;                             //  line from (1,1)
         pix2 = pixels + py * pattrs + qx * 3;                                   //  line from (qx,qy)
         diff += abs(pix1[0] - pix2[0])
               + abs(pix1[1] - pix2[1])
               + abs(pix1[2] - pix2[2]);
      }

      if (diff < mindiff) {                                                      //  save best match found
         mindiff = diff;
         sx = px;                                                                //  pattern origin
         sy = py;
      }
   }

   pattww = 2 * (sx - 1);                                                        //  set width, height to match
   patthh = 2 * (sy - 1);                                                        //    pattern size

   return;
}


//  combine image and pattern

void pattern_image()                                                             //  19.0
{
   using namespace patternnames;

   char        *tmap;                                                            //  maps tiles overlapping image pixel
   int         ii, cc, dist;
   int         tcol, trow, Ntcols, Ntrows;                                       //  tile columns and rows
   int         pww, phh, prs;                                                    //  pattern/tile dimensions
   int         mpx, mpy, tpx, tpy;
   int         R, G, B;
   uint8       *pixt;
   float       *pix1, *pix3;
   float       pbrite, f1, f2;
   double      mbrite = 0;
   float       *tbmap;
   
   pww = pattww;                                                                 //  capture and freeze volatile params
   phh = patthh;
   prs = pattrs;

   cc = e3ww * e3hh;                                                             //  tile pixels mapping to image pixel
   tmap = (char *) zmalloc(cc);
   memset(tmap,0,cc);

   cc = pww * phh * sizeof(float);                                               //  tile brightness map 
   tbmap = (float *) zmalloc(cc);

   for (mpy = 0; mpy < e3hh; mpy++)                                              //  clear output image to black
   for (mpx = 0; mpx < e3ww; mpx++)
   {
      pix3 = PXMpix(E3pxm,mpx,mpy);
      pix3[0] = pix3[1] = pix3[2] = 0;
   }

   Ntrows = e3hh / (phh-2*olaphh) + 1;                                           //  tile rows and columns including
   Ntcols = e3ww / (pww-2*olapww) + 1;                                           //    top/bottom and left/right overlaps

   for (trow = 0; trow < Ntrows; trow++)                                         //  loop tile rows, columns
   for (tcol = 0; tcol < Ntcols; tcol++)
   {
      for (tpy = 0; tpy < phh; tpy++)                                            //  loop tile pixels
      for (tpx = 0; tpx < pww; tpx++)
      {
         mpy = trow * (phh-2*olaphh) + tpy;                                      //  corresponding image pixel
         mpx = tcol * (pww-2*olapww) + tpx;
         if (mpy >= e3hh || mpx >= e3ww) continue;
         ii = mpy * e3ww + mpx;                                                  //  count tile pixels overlapping
         ++tmap[ii];                                                             //    this image pixel
      }
   }

   for (trow = 0; trow < Ntrows; trow++)                                         //  loop tile rows, columns
   for (tcol = 0; tcol < Ntcols; tcol++)
   {
      for (tpy = 0; tpy < phh; tpy++)                                            //  loop tile pixels
      for (tpx = 0; tpx < pww; tpx++)
      {
         mpy = trow * (phh-2*olaphh) + tpy;                                      //  corresponding image pixel
         mpx = tcol * (pww-2*olapww) + tpx;
         if (mpy >= e3hh || mpx >= e3ww) continue;
         ii = mpy * e3ww + mpx;
         pixt = pixels + tpy * prs + tpx * 3;                                    //  input tile pixel
         R = pixt[0];
         G = pixt[1];
         B = pixt[2];
         if (Fgray) R = G = B = 0.333 * (R + G + B);                             //  convert to grayscale               20.0
         pix3 = PXMpix(E3pxm,mpx,mpy);                                           //  output image pixel
         pix3[0] += (0.01 * opacity / tmap[ii]) * R;                             //  image = tile * opacity 0-100%
         pix3[1] += (0.01 * opacity / tmap[ii]) * G;                             //  reduce for overlapping tiles
         pix3[2] += (0.01 * opacity / tmap[ii]) * B;
      }
   }

   for (mpy = 0; mpy < e3hh; mpy++)                                              //  loop image pixels
   for (mpx = 0; mpx < e3ww; mpx++)
   {
      pix1 = PXMpix(E1pxm,mpx,mpy);                                              //  input image pixel
      pix3 = PXMpix(E3pxm,mpx,mpy);                                              //  output image pixel
      pix3[0] += (1.0 - 0.01 * opacity) * pix1[0];                               //  add input pixel to output,
      pix3[1] += (1.0 - 0.01 * opacity) * pix1[1];                               //    part not taken by tiles
      pix3[2] += (1.0 - 0.01 * opacity) * pix1[2];
   }
   
   for (tpy = 0; tpy < phh; tpy++)                                               //  loop tile pixels
   for (tpx = 0; tpx < pww; tpx++)
   {
      pixt = pixels + tpy * prs + tpx * 3;
      mbrite += pixt[0] + pixt[1] + pixt[2];                                     //  sum of all pixel RGB values
   }
   
   mbrite = mbrite / (phh * pww);                                                //  mean RGB sum for all tile pixels
   
   for (tpy = 0; tpy < phh; tpy++)                                               //  loop tile pixels
   for (tpx = 0; tpx < pww; tpx++)
   {
      pixt = pixels + tpy * prs + tpx * 3;
      pbrite = pixt[0] + pixt[1] + pixt[2];
      pbrite = pbrite / mbrite;                                                  //  pixel relative brightness 0...2
      ii = tpy * pww + tpx; 
      tbmap[ii] = pbrite;
   }

   for (trow = 0; trow < Ntrows; trow++)                                         //  loop tile rows, columns 
   for (tcol = 0; tcol < Ntcols; tcol++)
   {
      for (tpy = 0; tpy < phh; tpy++)                                            //  loop tile pixels
      for (tpx = 0; tpx < pww; tpx++)
      {
         mpy = trow * (phh-2*olaphh) + tpy;                                      //  corresponding image pixel
         mpx = tcol * (pww-2*olapww) + tpx;
         if (mpy >= e3hh || mpx >= e3ww) continue;
         pix3 = PXMpix(E3pxm,mpx,mpy);
         ii = tpy * pww + tpx;
         pbrite = tbmap[ii];                                                     //  tile pixel rel. brightness 0...2
         ii = mpy * e3ww + mpx;
         if (tmap[ii] > 1)                                                       //  if 2+ tile pixels map to image pixel
            pbrite = (pbrite - 1.0) / tmap[ii] + 1.0;                            //    reduce impact of each
         pbrite = 0.01 * contrast * (pbrite - 1.0) + 1.0;                        //  apply contrast factor 0-100%
         pix3[0] *= pbrite;
         pix3[1] *= pbrite;
         pix3[2] *= pbrite;
      }
   }
   
   for (mpy = 0; mpy < e3hh; mpy++)                                              //  stop RGB overflows
   for (mpx = 0; mpx < e3ww; mpx++)
   {
      pix3 = PXMpix(E3pxm,mpx,mpy);
      if (pix3[0] > 255) pix3[0] = 255;
      if (pix3[1] > 255) pix3[1] = 255;
      if (pix3[2] > 255) pix3[2] = 255;
   }

   if (sa_stat == 3)                                                             //  select area active
   {
      for (mpy = 0; mpy < e3hh; mpy++)                                           //  loop image pixels
      for (mpx = 0; mpx < e3ww; mpx++)
      {
         ii = mpy * e3ww + mpx;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (dist) {                                                             //  pixel inside area
            if (dist < sa_blendwidth)
               f1 = sa_blendfunc(dist);                                          //  blend edges
            else f1 = 1.0;
         }
         else f1 = 0.0;                                                          //  pixel outside area
         f2 = 1.0 - f1;
         pix1 = PXMpix(E1pxm,mpx,mpy);                                           //  input image pixel
         pix3 = PXMpix(E3pxm,mpx,mpy);                                           //  output image pixel
         pix3[0] = f1 * pix3[0] + f2 * pix1[0];
         pix3[1] = f1 * pix3[1] + f2 * pix1[1];
         pix3[2] = f1 * pix3[2] + f2 * pix1[2];
      }
   }

   return;
}


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

//  Create a mosaic image using tiny thumbnails as tiles.
//  tiles can be clicked to pop-up a larger zoomable image.

namespace mosaic_names
{
   editfunc EFmosaic;                        //  edit function data

   #define  maxtiles maximages
   int      iww, ihh;                        //  base image dimensions
   int      tww, thh;                        //  tile size (default 32x24)
   int      mww, mhh;                        //  image size
   char     *tilefile[maxtiles];             //  tile files list (100K x 100 --> 10M)
   uint8    *tileimage[maxtiles];            //  tile images (tww x thh x 3) (100K x 32 x 24 x 3 --> 230M)
   int      Ntiles, Ndone;                   //  tile image count

   float    tileRGB1[maxtiles][3];           //  tile mean RGB values for each tile quadrant
   float    tileRGB2[maxtiles][3];           //  (100K x 3 x 4B --> 1.2M x 4Q --> 4.8M)
   float    tileRGB3[maxtiles][3];
   float    tileRGB4[maxtiles][3];

   int      *tilemap;                        //  maps image [px,py] to tile used at that position
   int      tbusy[max_threads];
}


//  menu function

void m_mosaic(GtkWidget *, const char *)
{
   using namespace mosaic_names;

   int   mosaic_dialog_event(zdialog*, cchar *);
   void  mosaic_mousefunc();

   zdialog  *zd;
   cchar    *title = E2X("Create Mosaic");
   char     label[12];
   int      cc;

   F1_help_topic = "mosaic";

   EFmosaic.menufunc = m_mosaic;
   EFmosaic.funcname = "mosaic";                                                 //  function name
   EFmosaic.mousefunc = mosaic_mousefunc;                                        //  mouse function

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

   tww = 32;                                                                     //  default tile size
   thh = 24;
   Ntiles = 0;                                                                   //  tile file and image counts

   iww = E3pxm->ww;                                                              //  base image dimensions
   ihh = E3pxm->hh;

   cc = maxtiles * sizeof(char *);                                               //  clear tile file list
   memset(tilefile,0,cc);

   cc = maxtiles * sizeof(uint8 *);                                              //  clear tile image list
   memset(tileimage,0,cc);

   cc = iww * ihh * sizeof(int);                                                 //  allocate image/tile map
   tilemap = (int *) zmalloc(cc);
   memset(tilemap,-1,cc);

/***
       ____________________________________________
      |               Create Mosaic                |
      |                                            |
      |   Tile Width [___]  Height [___]           |
      |   [Tiles] nnnnn   [Image]                  |
      |   Tile blending   =========[]============= |
      |                                            |
      |                          [done] [cancel]   |
      |____________________________________________|

***/

   zd = zdialog_new(title,Mwin,Bdone,Bcancel,null);                              //  mosaic dialog
   CEF->zd = zd;

   zdialog_add_widget(zd,"hbox","hbsize","dialog");
   zdialog_add_widget(zd,"label","labsize","hbsize",E2X("Tile"),"space=3");
   zdialog_add_widget(zd,"label","labwidth","hbsize",Bwidth,"space=3");
   zdialog_add_widget(zd,"zspin","width","hbsize","16|48|2|32");
   zdialog_add_widget(zd,"label","space","hbsize",0,"space=5");
   zdialog_add_widget(zd,"label","labheight","hbsize",Bheight,"space=3");
   zdialog_add_widget(zd,"zspin","height","hbsize","16|48|2|24");

   zdialog_add_widget(zd,"hbox","hbcreate","dialog");
   zdialog_add_widget(zd,"button","tiles","hbcreate",E2X("Tiles"),"space=3");
   zdialog_add_widget(zd,"label","labNtiles","hbcreate","0","space=5");
   zdialog_add_widget(zd,"button","image","hbcreate",Bimage,"space=10");

   zdialog_add_widget(zd,"vbox","space","dialog",0,"space=3");
   zdialog_add_widget(zd,"hbox","hbblend","dialog");
   zdialog_add_widget(zd,"label","labblend","hbblend",E2X("Tile blending"),"space=3");
   zdialog_add_widget(zd,"hscale","blend","hbblend","0|100|1|0","space=5|expand");

   zdialog_stuff(zd,"width",tww);
   zdialog_stuff(zd,"height",thh);

   snprintf(label,12,"%d",Ntiles);
   zdialog_stuff(zd,"labNtiles",label);

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


//  mosaic dialog event and completion function

int mosaic_dialog_event(zdialog *zd, const char *event)
{
   using namespace mosaic_names;
   
   void * mosaic_thread1(void *arg);
   void * mosaic_thread2(void *arg);
   void   mosaic_mousefunc();

   static int  block = 0;
   char        **flist = 0;
   char        label[12];
   int         ii, err, NF;
   int         trow, tcol, tpx, tpy, ipx, ipy;
   uint8       *timage, *tpix;
   float       blend, f1, f3;
   float       *pix1, *pix3;

   if (block) return 1;
   block = 1;   

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      for (ii = 0; ii < Ntiles; ii++) {                                          //  free memory
         if (tilefile[ii]) zfree(tilefile[ii]);
         if (tileimage[ii]) zfree(tileimage[ii]);
      }
      if (tilemap) zfree(tilemap);
      block = 0;
      return 1;
   }

   if (strmatch(event,"focus")) {
      takeMouse(mosaic_mousefunc,dragcursor);                                    //  connect mouse function
      block = 0;
      return 1;
   }

   if (strmatch(event,"tiles"))                                                  //  read thumbnails, create tile images
   {
      for (ii = 0; ii < Ntiles; ii++) {                                          //  free memory
         if (tilefile[ii]) zfree(tilefile[ii]);
         if (tileimage[ii]) zfree(tileimage[ii]);
         tilefile[ii] = 0;
         tileimage[ii] = 0;
      }

      Ntiles = 0;
      Ndone = 0;                                                                 //  progress counter

      zdialog_stuff(zd,"labNtiles","0");

      zdialog_fetch(zd,"width",tww);                                             //  get tile size from dialog
      zdialog_fetch(zd,"height",thh);

      err = find_imagefiles(thumbfolder,2+16,flist,NF);                          //  all thumbnails, all levels 
      if (err) {
         zmessageACK(Mwin,strerror(errno));
         block = 0;
         return 1;
      }

      if (NF > maxtiles) {                                                       //  too many
         zmessageACK(Mwin,E2X("exceeded max. tiles: %d"),maxtiles);
         zfree(flist);
         block = 0;
         return 1;
      }

      if (NF < 100) {                                                            //  too few
         zmessageACK(Mwin,E2X("only %d tile images found"),NF);
         zfree(flist);
         block = 0;
         return 1;
      }

      for (ii = 0; ii < NF; ii++) {                                              //  save thumbnail filespecs
         tilefile[ii] = flist[ii];
         tileimage[ii] = 0;
      }

      zfree(flist);

      Ntiles = NF;

      for (ii = 0; ii < NWT; ii++) {                                             //  create tile images and 
         tbusy[ii] = 1;                                                          //    RGB color data per tile
         start_detached_thread(mosaic_thread1,&Nval[ii]);
      }

      for (ii = 0; ii < NWT; ii++) {                                             //  wait for completion
         while(tbusy[ii]) {
            zsleep(0.01);
            snprintf(label,12,"%d",Ndone);                                       //  show count in dialog
            zdialog_stuff(zd,"labNtiles",label);
            zmainloop();
         }
      }

      block = 0;
      return 1;
   }

   if (strmatch(event,"image"))                                                  //  apply tiles to mosaic image
   {
      if (! Ntiles) {
         block = 0;
         return 1;
      }

      edit_undo();                                                               //  reset to original image

      for (ii = 0; ii < NWT; ii++) {                                             //  apply tiles to image
         tbusy[ii] = 1;
         start_detached_thread(mosaic_thread2,&Nval[ii]);
      }

      for (int ii = 0; ii < NWT; ii++) {                                         //  wait for completion
         while(tbusy[ii]) {
            zsleep(0.01);
            Fpaint2();                                                           //  update image continuously
            zmainloop();
         }
      }
      
      zdialog_stuff(zd,"blend",0);                                               //  reset blend control

      CEF->Fmods++;                                                              //  image is modified
      CEF->Fsaved = 0;
      block = 0;
      return 1;
   }
   
   if (strmatch(event,"blend"))
   {
      if (! Ntiles) {
         block = 0;
         return 1;
      }

      zdialog_fetch(zd,"blend",blend);                                           //  get blend value 0-100
      f1 = blend / 100.0;                                                        //  base image part, 0.0 --> 1.0
      f3 = 1.0 - f1;                                                             //  tile image part, 1.0 --> 0.0

      for (trow = 0; trow < ihh/thh; trow++)                                     //  loop tile positions in base image
      for (tcol = 0; tcol < iww/tww; tcol++)
      {
         for (tpy = 0; tpy < thh; tpy++)                                         //  loop tile pixels
         for (tpx = 0; tpx < tww; tpx++)
         {
            ipy = trow * thh + tpy;                                              //  corresponding image pixels
            ipx = tcol * tww + tpx;
            ii = iww * ipy + ipx;                                                //  get tile used at image pixel
            ii = tilemap[ii];
            timage = tileimage[ii];
            tpix = timage + tpy * tww * 3 + tpx * 3;                             //  input tile pixel
            pix1 = PXMpix(E1pxm,ipx,ipy);                                        //  input E1 image pixel
            pix3 = PXMpix(E3pxm,ipx,ipy);                                        //  output E3 image pixel
            pix3[0] = f1 * pix1[0] + f3 * tpix[0];                               //  replace image pixels with tile pixels
            pix3[1] = f1 * pix1[1] + f3 * tpix[1];
            pix3[2] = f1 * pix1[2] + f3 * tpix[2];
         }
      }
      
      Fpaint2();

      CEF->Fmods++;                                                              //  image is modified
      CEF->Fsaved = 0;
      block = 0;
      return 1;
   }

   block = 0;
   return 1;
}


//  thread to make tile images from thumbnails and tile RGB color data

void * mosaic_thread1(void *arg)
{
   using namespace mosaic_names;

   int      index = *((int *) arg);
   PIXBUF   *pxb1, *pxb2;
   GError   *gerror = 0;
   int      tpx, tpy;
   int      tww1, thh1, tww2, thh2;
   int      ii, row, rs;
   uint8    *tpixels, *tpix, *timage;
   uint8    *row1, *row2;

   float    fred1, fgreen1, fblue1;
   float    fred2, fgreen2, fblue2;
   float    fred3, fgreen3, fblue3;
   float    fred4, fgreen4, fblue4;
   float    coeff;

   for (ii = index; ii < Ntiles; ii += NWT)                                      //  loop tile image files
   {
      if (tileimage[ii]) zfree(tileimage[ii]);
      tileimage[ii] = 0;

      pxb1 = gdk_pixbuf_new_from_file_at_size(tilefile[ii],64,64,&gerror);       //  create pixbuf within 64x64
      if (! pxb1) {
         printz("file: %s \n %s",tilefile[ii],gerror->message);
         gerror = 0;
         continue;
      }

      tww1 = gdk_pixbuf_get_width(pxb1);                                         //  actual width, height (max. 64)
      thh1 = gdk_pixbuf_get_height(pxb1);
      g_object_unref(pxb1);
      
      if (tww1 < tww || thh1 < thh) continue;                                    //  unusable                           19.0

      if (tww1 * thh < tww * thh1) {                                             //  tww1/thh1 < tww/thh
         tww2 = tww;                                                             //    (too high for tww x thh)
         thh2 = tww * thh1 / tww1;
      }
      else {                                                                     //  tww1/thh1 > tww/thh
         thh2 = thh;                                                             //    (too wide for tww x thh)
         tww2 = thh * tww1 / thh1;
      }
      
      pxb2 = gdk_pixbuf_new_from_file_at_size(tilefile[ii],tww2+1,thh2,&gerror); //  tww2+1 required (see below)        19.0
      if (! pxb2) {
         printz("file: %s \n %s",tilefile[ii],gerror->message);
         gerror = 0;
         continue;
      }
      
      tpx = (tww2 - tww) / 2;                                                    //  tww2 or thh2 offset for pixels
      tpy = (thh2 - thh) / 2;                                                    //    copied to tww x thh tile image
      
      rs = gdk_pixbuf_get_rowstride(pxb2);
      tpixels = gdk_pixbuf_get_pixels(pxb2);

      timage = (uint8 *) zmalloc(tww * thh * 3);                                 //  allocate memory for tww x thh pixels
      
      for (row = 0; row < thh; row++)
      {                                                                          //  copy pixbuf tww x thh section
         row1 = tpixels + rs * (row + tpy) + tpx * 3;
         row2 = timage + row * tww * 3;
         memcpy(row2,row1,tww * 3);                                              //  buffer-overflow unless (see above)
      }

      g_object_unref(pxb2);

      tileimage[ii] = timage;                                                    //  save final tile image

      fred1 = fgreen1 = fblue1 = 0;
      fred2 = fgreen2 = fblue2 = 0;
      fred3 = fgreen3 = fblue3 = 0;
      fred4 = fgreen4 = fblue4 = 0;

      for (tpy = 0; tpy < thh/2; tpy++)                                          //  loop pixels in top left tile quadrant
      for (tpx = 0; tpx < tww/2; tpx++)
      {
         tpix = timage + tpy * tww * 3 + tpx * 3;
         fred1 += tpix[0];                                                       //  sum RGB values
         fgreen1 += tpix[1];
         fblue1 += tpix[2];
      }

      for (tpy = 0; tpy < thh/2; tpy++)                                          //  top right
      for (tpx = tww/2; tpx < tww; tpx++)
      {
         tpix = timage + tpy * tww * 3 + tpx * 3;
         fred2 += tpix[0];
         fgreen2 += tpix[1];
         fblue2 += tpix[2];
      }

      for (tpy = thh/2; tpy < thh; tpy++)                                        //  bottom left
      for (tpx = 0; tpx < tww/2; tpx++)
      {
         tpix = timage + tpy * tww * 3 + tpx * 3;
         fred3 += tpix[0];
         fgreen3 += tpix[1];
         fblue3 += tpix[2];
      }

      for (tpy = thh/2; tpy < thh; tpy++)                                        //  bottom right
      for (tpx = tww/2; tpx < tww; tpx++)
      {
         tpix = timage + tpy * tww * 3 + tpx * 3;
         fred4 += tpix[0];
         fgreen4 += tpix[1];
         fblue4 += tpix[2];
      }

      coeff = 0.25 / (tww * thh);

      tileRGB1[ii][0] = coeff * fred1;                                           //  save tile mean RGB values
      tileRGB1[ii][1] = coeff * fgreen1;                                         //    for each quadrant
      tileRGB1[ii][2] = coeff * fblue1;

      tileRGB2[ii][0] = coeff * fred2;
      tileRGB2[ii][1] = coeff * fgreen2;
      tileRGB2[ii][2] = coeff * fblue2;

      tileRGB3[ii][0] = coeff * fred3;
      tileRGB3[ii][1] = coeff * fgreen3;
      tileRGB3[ii][2] = coeff * fblue3;

      tileRGB4[ii][0] = coeff * fred4;
      tileRGB4[ii][1] = coeff * fgreen4;
      tileRGB4[ii][2] = coeff * fblue4;
      
      Ndone++;                                                                   //  progress counter
   }
   
   tbusy[index] = 0;
   pthread_exit(0);
}


//  thread to apply best color-matched tile to each image position

void * mosaic_thread2(void *arg) 
{
   using namespace mosaic_names;

   int      index = *((int *) arg);
   int      trow, tcol, tpx, tpy, ipx, ipy;
   int      ii, bestii;
   uint8    *tpix, *timage;
   float    *pix1, *pix3;

   float    fred1, fgreen1, fblue1;
   float    fred2, fgreen2, fblue2;
   float    fred3, fgreen3, fblue3;
   float    fred4, fgreen4, fblue4;

   float    match, bestmatch;
   float    coeff;

   for (trow = index; trow < ihh/thh; trow += NWT)                               //  loop tile positions in base image
   for (tcol = 0; tcol < iww/tww; tcol++)
   {
      fred1 = fgreen1 = fblue1 = 0;
      fred2 = fgreen2 = fblue2 = 0;
      fred3 = fgreen3 = fblue3 = 0;
      fred4 = fgreen4 = fblue4 = 0;

      for (tpy = 0; tpy < thh/2; tpy++)                                          //  loop pixels in top left tile quadrant
      for (tpx = 0; tpx < tww/2; tpx++)
      {
         ipy = trow * thh + tpy;                                                 //  corresponding image pixels
         ipx = tcol * tww + tpx;
         pix1 = PXMpix(E1pxm,ipx,ipy);
         fred1 += pix1[0];                                                       //  sum image RGB values
         fgreen1 += pix1[1];
         fblue1 += pix1[2];
      }

      for (tpy = 0; tpy < thh/2; tpy++)                                          //  top right quadrant
      for (tpx = tww/2; tpx < tww; tpx++)
      {
         ipy = trow * thh + tpy;
         ipx = tcol * tww + tpx;
         pix1 = PXMpix(E1pxm,ipx,ipy);
         fred2 += pix1[0];
         fgreen2 += pix1[1];
         fblue2 += pix1[2];
      }

      for (tpy = thh/2; tpy < thh; tpy++)                                        //  lower left quadrant
      for (tpx = 0; tpx < tww/2; tpx++)
      {
         ipy = trow * thh + tpy;
         ipx = tcol * tww + tpx;
         pix1 = PXMpix(E1pxm,ipx,ipy);
         fred3 += pix1[0];
         fgreen3 += pix1[1];
         fblue3 += pix1[2];
      }

      for (tpy = thh/2; tpy < thh; tpy++)                                        //  lower right quadrant
      for (tpx = tww/2; tpx < tww; tpx++)
      {
         ipy = trow * thh + tpy;
         ipx = tcol * tww + tpx;
         pix1 = PXMpix(E1pxm,ipx,ipy);
         fred4 += pix1[0];
         fgreen4 += pix1[1];
         fblue4 += pix1[2];
      }

      coeff = 0.25 / (tww * thh);

      fred1 = coeff * fred1;                                                     //  mean image RGB values for
      fgreen1 = coeff * fgreen1;                                                 //    each quadrant
      fblue1 = coeff * fblue1;

      fred2 = coeff * fred2;
      fgreen2 = coeff * fgreen2;
      fblue2 = coeff * fblue2;

      fred3 = coeff * fred3;
      fgreen3 = coeff * fgreen3;
      fblue3 = coeff * fblue3;

      fred4 = coeff * fred4;
      fgreen4 = coeff * fgreen4;
      fblue4 = coeff * fblue4;

      bestmatch = 0;
      bestii = 0;

      for (ii = 0; ii < Ntiles; ii++)                                            //  loop tile RGB values
      {
         if (! tileimage[ii]) continue;

         match = RGBMATCH(fred1,fgreen1,fblue1,                                  //  0..1 = zero..perfect match
                    tileRGB1[ii][0],tileRGB1[ii][1],tileRGB1[ii][2]);
         match += RGBMATCH(fred2,fgreen2,fblue2,
                    tileRGB2[ii][0],tileRGB2[ii][1],tileRGB2[ii][2]);
         match += RGBMATCH(fred3,fgreen3,fblue3,
                    tileRGB3[ii][0],tileRGB3[ii][1],tileRGB3[ii][2]);
         match += RGBMATCH(fred4,fgreen4,fblue4,
                    tileRGB4[ii][0],tileRGB4[ii][1],tileRGB4[ii][2]);

         if (match > bestmatch) {
            bestmatch = match;                                                   //  save best matching tile
            bestii = ii;
         }
      }

      ii = bestii;                                                               //  best matching tile
      timage = tileimage[ii];

      for (tpy = 0; tpy < thh; tpy++)                                            //  loop tile pixels
      for (tpx = 0; tpx < tww; tpx++)
      {
         ipy = trow * thh + tpy;                                                 //  corresponding image pixels
         ipx = tcol * tww + tpx;
         tpix = timage + tpy * tww * 3 + tpx * 3;
         pix1 = PXMpix(E1pxm,ipx,ipy);
         pix3 = PXMpix(E3pxm,ipx,ipy);
         pix3[0] = tpix[0];                                                      //  replace image pixels with tile pixels
         pix3[1] = tpix[1];
         pix3[2] = tpix[2];
         ii = iww * ipy + ipx;                                                   //  save map of tile used 
         tilemap[ii] = bestii;                                                   //    at each image pixel
      }
   }

   tbusy[index] = 0;
   pthread_exit(0);
}


//  mouse function - called for mouse events inside image

void mosaic_mousefunc()
{
   using namespace mosaic_names;

   int      ii;
   char     *tfile, *mfile;

   if (! LMclick) return;
   LMclick = 0;

   ii = Myclick * iww + Mxclick;
   ii = tilemap[ii];
   if (ii < 0) return;
   tfile = tilefile[ii];
   mfile = thumb2imagefile(tfile);
   if (mfile) popup_image(mfile,MWIN,1,256);
   else popup_image(tfile,MWIN,1,256);
   return;
}


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

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

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


//  menu function

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

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

   F1_help_topic = "color mode";

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

   if (! edit_setup(EFcolormode)) return;                                        //  setup edit: no preview
   
/***
          _____________________________
         |        Color Mode           |
         |                             |
         | [_] reset                   |                                         //  check boxes
         | [_] black/white positive    |
         | [_] black/white negative    |
         | [_] color negative          |
         | [_] RGB -> GBR              |
         | [_] RGB -> BRG              |
         | [_] sepia                   |
         |                             |
         |  0% ===========[]==== 100%  |
         |                             |
         |             [Done] [Cancel] |
         |_____________________________|

***/

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

   zdialog_add_widget(zd,"check","reset","dialog",E2X("reset"));
   zdialog_add_widget(zd,"check","b&wpos","dialog",E2X("black/white positive"));
   zdialog_add_widget(zd,"check","b&wneg","dialog",E2X("black/white negative"));
   zdialog_add_widget(zd,"check","colneg","dialog",E2X("color negative"));
   zdialog_add_widget(zd,"check","rgb-gbr","dialog",E2X("RGB -> GBR"));          //                                     19.5
   zdialog_add_widget(zd,"check","rgb-brg","dialog",E2X("RGB -> BRG"));
   zdialog_add_widget(zd,"check","sepia","dialog",E2X("sepia"));
   zdialog_add_widget(zd,"hbox","hbblend","dialog");
   zdialog_add_widget(zd,"label","lab0","hbblend","0%","space=5");
   zdialog_add_widget(zd,"hscale","blend","hbblend","0.0|1.0|0.01|1.0","expand");
   zdialog_add_widget(zd,"label","lab100","hbblend","100%","space=5");

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

   return;
}


//  dialog event and completion callback function

int colormode_dialog_event(zdialog *zd, cchar *event)
{
   using namespace colormode_names;
   
   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                   19.5
      edit_fullsize();                                                           //  get full size image
      signal_thread();
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {
         edit_fullsize();                                                        //  get full size image                19.5
         signal_thread();
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }
   
   if (strmatch(event,"focus")) return 1;
   
   if (zstrstr("reset b&wpos b&wneg colneg rgb-gbr rgb-brg sepia",event))
   {
      zdialog_stuff(zd,"reset",0);
      zdialog_stuff(zd,"b&wpos",0);
      zdialog_stuff(zd,"b&wneg",0);
      zdialog_stuff(zd,"colneg",0);
      zdialog_stuff(zd,"rgb-gbr",0);                                             //  19.5
      zdialog_stuff(zd,"rgb-brg",0);
      zdialog_stuff(zd,"sepia",0);
      zdialog_stuff(zd,event,1);
      if (strmatch(event,"reset")) mode = 0;
      if (strmatch(event,"b&wpos")) mode = 1;
      if (strmatch(event,"b&wneg")) mode = 2;
      if (strmatch(event,"colneg")) mode = 3;
      if (strmatch(event,"rgb-gbr")) mode = 4;
      if (strmatch(event,"rgb-brg")) mode = 5;
      if (strmatch(event,"sepia")) mode = 6;
   }
   
   zdialog_fetch(zd,"blend",blend);
   
   if (mode == 0) {
      edit_reset();
      return 1;
   }

   paintlock(1);                                                                 //  block window paint                 19.5

   signal_thread();
   wait_thread_idle();                                                           //  wait for thread idle

   paintlock(0);                                                                 //  unblock window paint               19.5
   Fpaint2();                                                                    //  update window

   return 1;
}


//  thread function

void * colormode_thread(void *)
{
   using namespace colormode_names;

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

      do_wthreads(colormode_wthread,NWT);                                        //  worker threads

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

   return 0;                                                                     //  not executed, stop warning
}


//  worker thread functions

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

   int         index = *((int *) (arg));
   int         E3ww, E3hh;
   int         ii, dist, px, py;
   float       red1, green1, blue1;
   float       red3, green3, blue3;
   float       *pix1, *pix3;
   float       brite, ff1, ff2;
   
   E3ww = E3pxm->ww;                                                             //  19.5
   E3hh = E3pxm->hh;

   dist = 0;                                                                     //  stop compiler warnings

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

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

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

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

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

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

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

         case 5: {                                                               //  RGB - BRG                          19.5
            red3 = blue1;
            green3 = red1;
            blue3 = green1;
            break;
         }

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

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

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

            brite = 0.8333 * (brite + 0.2);                                      //  add brightness at low end
            red3 = red3 * brite;                                                 //  output = combined color * brightness
            green3 = green3 * brite;
            blue3 = blue3 * brite;
            break;
         }
      }
      
      if (red3 < 0) red3 = 0;                                                    //  prevent underflow/overlfow
      if (red3 > 255.9) red3 = 255.9;
      if (green3 < 0) green3 = 0;
      if (green3 > 255.9) green3 = 255.9;
      if (blue3 < 0) blue3 = 0;
      if (blue3 > 255.9) blue3 = 255.9;
      
      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  blend changes over blendwidth
         ff1 = sa_blendfunc(dist);
         ff2 = 1.0 - ff1;
         red3 = ff1 * red3 + ff2 * red1;
         green3 = ff1 * green3 + ff2 * green1;
         blue3 = ff1 * blue3 + ff2 * blue1;
      }
      
      if (blend < 1.0) {                                                         //  blend slider
         ff1 = blend;
         ff2 = 1.0 - ff1;
         red3 = ff1 * red3 + ff2 * red1;
         green3 = ff1 * green3 + ff2 * green1;
         blue3 = ff1 * blue3 + ff2 * blue1;
      }

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

   pthread_exit(0);
}


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

//  image color-depth reduction

namespace color_depth_names
{
   int         color_depth_depth = 16;                                           //  bits per RGB color
   editfunc    EFcolor_depth;
   int         E3ww, E3hh;
}


//  menu function

void m_color_depth(GtkWidget *, cchar *menu)
{
   using namespace color_depth_names;

   int    color_depth_dialog_event(zdialog *zd, cchar *event);
   void * color_depth_thread(void *);

   cchar  *colmess = E2X("Set color depth to 1-16 bits");

   F1_help_topic = "color depth";

   EFcolor_depth.menuname = menu;
   EFcolor_depth.menufunc = m_color_depth;
   EFcolor_depth.funcname = "color_depth";
   EFcolor_depth.FprevReq = 1;                                                   //  use preview
   EFcolor_depth.Farea = 2;                                                      //  select area usable
   EFcolor_depth.FusePL = 1;                                                     //  use with paint/lever edits OK
   EFcolor_depth.Fscript = 1;                                                    //  scripting supported
   EFcolor_depth.threadfunc = color_depth_thread;                                //  thread function
   if (! edit_setup(EFcolor_depth)) return;                                      //  setup edit
   
   E3ww = E3pxm->ww;
   E3hh = E3pxm->hh;
   
/***
          _____________________________
         |   Set Color Depth           |
         |                             |
         | Set color depth 1-16 bits   |
         | [___]                       |
         |                             |
         |             [Done] [Cancel] |
         |_____________________________|

***/

   zdialog *zd = zdialog_new(E2X("Set Color Depth"),Mwin,Bdone,Bcancel,null);
   EFcolor_depth.zd = zd;
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",colmess,"space=3");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"zspin","colors","hb2","1|16|1|16","space=5");

   color_depth_depth = 16;

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

   return;
}


//  dialog event and completion callback function

int color_depth_dialog_event(zdialog *zd, cchar *event)
{
   using namespace color_depth_names;

   if (strmatch(event,"focus")) return 1;
   if (strmatch(event,"apply")) event = "colors";                                //  from script
   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();
      E3ww = E3pxm->ww;
      E3hh = E3pxm->hh;
      signal_thread();
      return 1;
   }

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

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

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

   return 0;
}


//  image color depth thread function

void * color_depth_thread(void *)
{
   using namespace color_depth_names;

   int         ii, px, py, rgb, dist = 0;
   uint16      m1, m2, val1, val3;
   float       *pix1, *pix3;
   float       f1, f2;

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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      m1 = 0xFFFF << (16 - color_depth_depth);                                   //  5 > 1111100000000000
      m2 = 0x8000 >> color_depth_depth;                                          //  5 > 0000010000000000

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

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

         for (rgb = 0; rgb < 3; rgb++)
         {
            val1 = 256 * pix1[rgb];                                              //  16 bit integer <= 65535
            val3 = (val1 + m2) & m1;                                             //  round: nearest value within bits
            if (val3 > m1) val3 = m1;

            if (sa_stat == 3 && dist < sa_blendwidth) {                          //  select area is active,
               f2 = sa_blendfunc(dist);                                          //    blend changes over sa_blendwidth
               f1 = 1.0 - f2;
               val3 = int(f1 * val1 + f2 * val3);
            }

            pix3[rgb] = val3 / 256;
         }
      }

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

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

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


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

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

namespace shiftcolors_names
{
   editfunc    EFshiftcolors;                                                    //  edit function data
   float       shiftred = 0.5, shiftgreen = 0.5, shiftblue = 0.5;
   int         E3ww, E3hh;
}


//  menu function

void m_shift_colors(GtkWidget *, cchar *menu)
{
   using namespace shiftcolors_names;

   int    shiftcolors_dialog_event(zdialog* zd, const char *event);
   void * shiftcolors_thread(void *);

   F1_help_topic = "shift colors";

   EFshiftcolors.menuname = menu;
   EFshiftcolors.menufunc = m_shift_colors;
   EFshiftcolors.funcname = "shift_colors";                                      //  function name
   EFshiftcolors.FprevReq = 1;                                                   //  use preview
   EFshiftcolors.Farea = 2;                                                      //  select area OK
   EFshiftcolors.Fscript = 1;                                                    //  scripting supported
   EFshiftcolors.FusePL = 1;                                                     //  paint/lever edits supported
   EFshiftcolors.threadfunc = shiftcolors_thread;                                //  thread function

   if (! edit_setup(EFshiftcolors)) return;                                      //  setup edit
   
   E3ww = E3pxm->ww;
   E3hh = E3pxm->hh;

/***
                  Shift Colors

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

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

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

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

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

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

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

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

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

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


//  shift color dialog event and completion function

int shiftcolors_dialog_event(zdialog *zd, const char *event)
{
   using namespace shiftcolors_names;

   float    shiftall;

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   
   if (strmatch(event,"fullsize")) {                                             //  from select area
      if (! CEF->Fpreview) return 1;
      edit_fullsize();
      E3ww = E3pxm->ww;
      E3hh = E3pxm->hh;
      signal_thread();
      return 1;
   }

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

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

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

   return 1;
}


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

void * shiftcolors_thread(void *)
{
   using namespace shiftcolors_names;

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

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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      do_wthreads(shiftcolors_wthread,NWT);                                      //  worker threads

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

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop warning
}


void * shiftcolors_wthread(void *arg)                                            //  worker thread function
{
   using namespace shiftcolors_names;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

   pthread_exit(0);
}


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

//  alien colors - displace hue by an angle that varies over the image

namespace alien_colors_names
{
   editfunc    EFaliencolors;
   float       BLsz, Ampl;                                                       //  block size, amplitude
   float       Frand[200][200];                                                  //  random numbers
   #define maxblocks 200
}


//  menu function

void m_alien_colors(GtkWidget *, cchar *menu)
{
   using namespace alien_colors_names;

   int    alien_colors_dialog_event(zdialog* zd, const char *event);
   void * alien_colors_thread(void *);

   F1_help_topic = "alien colors";

   EFaliencolors.menuname = menu;
   EFaliencolors.menufunc = m_alien_colors;
   EFaliencolors.funcname = "alien_colors";
   EFaliencolors.Farea = 2;                                                      //  select area usable
   EFaliencolors.FprevReq = 1;                                                   //  use preview mode
   EFaliencolors.Fscript = 1;                                                    //  scripting supported
   EFaliencolors.threadfunc = alien_colors_thread;

   if (! edit_setup(EFaliencolors)) return;
   
/***
          __________________________
         |      Alien Colors        |
         |                          |
         |  blocksize  [___]        |
         |  amplitude  [___]        |
         |                          |
         |          [done] [cancel] |
         |__________________________|

***/

   zdialog *zd = zdialog_new(E2X("Alien Colors"),Mwin,Bdone,Bcancel,null);
   CEF->zd = zd;
   zdialog_add_widget(zd,"hbox","hbbsz","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labbsz","hbbsz",E2X("blocksize"),"space=5");
   zdialog_add_widget(zd,"zspin","BLsz","hbbsz","10|1000|1|100");
   zdialog_add_widget(zd,"hbox","hbamp","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labamp","hbamp",E2X("amplitude"),"space=5");
   zdialog_add_widget(zd,"zspin","Ampl","hbamp","0.0|1.0|0.01|1.0");

   zdialog_restore_inputs(zd);                                                   //  restore previous inputs
   zdialog_run(zd,alien_colors_dialog_event,"save");                             //  run dialog - parallel
   
   zdialog_send_event(zd,"BLsz");                                                //  initial image
   return;
}


//  alien dialog event and completion function

int alien_colors_dialog_event(zdialog *zd, const char *event)                    //  alien dialog event function
{
   using namespace alien_colors_names;

   if (strmatch(event,"focus")) return 1;
   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   
   if (zstrstr("BLsz Ampl Apply",event))
   {
      zdialog_fetch(zd,"BLsz",BLsz);                                             //  get user inputs
      zdialog_fetch(zd,"Ampl",Ampl);
      signal_thread();                                                           //  calculate
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  [done]
      {
         int ww = E3pxm->ww;                                                     //  preview size
         edit_fullsize();
         if (E3pxm->ww > ww) {
            BLsz *= 1.0 * E3pxm->ww / ww;                                        //  scale up for full size
            signal_thread();
         }
         edit_done(0);
      }
      else edit_cancel(0);                                                       //  discard edit
   }

   return 1;
}


//  thread function - multiple working threads to update image

void * alien_colors_thread(void *)
{
   using namespace alien_colors_names;

   void * alien_colors_wthread(void *);

   for (int ii = 0; ii < maxblocks; ii++)                                        //  populate random values
   for (int jj = 0; jj < maxblocks; jj++)
      Frand[ii][jj] = drandz();

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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      do_wthreads(alien_colors_wthread,NWT);                                     //  worker threads

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

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop warning
}


void * alien_colors_wthread(void *arg)                                           //  working threads
{
   using namespace alien_colors_names;

   int      index = *((int *) (arg));
   int      px, py, ww, hh;
   int      row, col, row1, col1, row2, col2;
   int      maxrow, maxcol, bsize, edist = 0;
   float    d1, d7, dx, dy;
   float    W0, W1, W2, W3, W4, W5, W6, W7, W8, Wsum;
   float    R1, G1, B1, R3, G3, B3, H, S, L;
   float    theta, f1, f2;
   float    *pix1, *pix3;

   ww = E3pxm->ww;
   hh = E3pxm->hh;
   
   bsize = BLsz;                                                                 //  block size
   if (ww / bsize >= maxblocks) bsize = ww / (maxblocks - 1);                    //  stop > maxblocks rows or cols
   if (hh / bsize >= maxblocks) bsize = hh / (maxblocks - 1);
   
   maxrow = 1 + hh / bsize;
   maxcol = 1 + ww / bsize;
   
   for (py = index; py < hh; py += NWT)
   for (px = 0; px < ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         int ii = py * ww + px;
         edist = sa_pixmap[ii];                                                  //  distance from edge
         if (! edist) continue;                                                  //  pixel outside area
      }
      
      d1 = py - py / bsize * bsize;                                              //  y coordinate within block
      d7 = px - px / bsize * bsize;                                              //  x coordinate

      d1 = d1 / bsize;                                                           //  rescale, 0 .. 1
      d7 = d7 / bsize;
      
      dx = d7 - 0.5;                                                             //  distance from pixel to center
      dy = d1 - 0.5;                                                             //    of 9 block group
      W0 = sqrtf(dx*dx + dy*dy);

      dx = d7 - 0.5;
      dy = d1 + 0.5;
      W1 = sqrtf(dx*dx + dy*dy);

      dx = 1.5 - d7;
      dy = d1 + 0.5;
      W2 = sqrtf(dx*dx + dy*dy);

      dx = 1.5 - d7;
      dy = 0.5 - d1;
      W3 = sqrtf(dx*dx + dy*dy);

      dx = 1.5 - d7;
      dy = 1.5 - d1;
      W4 = sqrtf(dx*dx + dy*dy);
      
      dx = 0.5 - d7;
      dy = 1.5 - d1;
      W5 = sqrtf(dx*dx + dy*dy);

      dx = d7 + 0.5;
      dy = 1.5 - d1;
      W6 = sqrtf(dx*dx + dy*dy);
      
      dx = d7 + 0.5;
      dy = 0.5 - d1;
      W7 = sqrtf(dx*dx + dy*dy);

      dx = d7 + 0.5;
      dy = d1 + 0.5;
      W8 = sqrtf(dx*dx + dy*dy);
      
      W0 = 1.5 - W0;                                                             //  block weight:
      W1 = 1.5 - W1;                                                             //    max. distance - actual
      W2 = 1.5 - W2;                                                             //  closer blocks have more weight
      W3 = 1.5 - W3;
      W4 = 1.5 - W4;
      W5 = 1.5 - W5;
      W6 = 1.5 - W6;
      W7 = 1.5 - W7;
      W8 = 1.5 - W8;
      
      if (W2 < 0) W2 = 0;                                                        //  corners may go negative
      if (W4 < 0) W4 = 0;
      if (W6 < 0) W6 = 0;
      if (W8 < 0) W8 = 0;
      
      Wsum = W0 + W1 + W2 + W3 + W4 + W5 + W6 + W7 + W8;                         //  sum of weights
      
      W0 = W0 / Wsum;                                                            //  normalize to make sum = 1
      W1 = W1 / Wsum;
      W2 = W2 / Wsum;
      W3 = W3 / Wsum;
      W4 = W4 / Wsum;
      W5 = W5 / Wsum;
      W6 = W6 / Wsum;
      W7 = W7 / Wsum;
      W8 = W8 / Wsum;
      
      row = py / bsize;                                                          //  block[row][col] for this px/py
      col = px / bsize;
      
      row1 = row2 = row;
      col1 = col2 = col;
      if (row > 0) row1 = row - 1;
      if (row < maxrow) row2 = row + 1;
      if (col > 0) col1 = col - 1;
      if (col < maxcol) col2 = col + 1;
      
      W0 = W0 * Frand[col][row];                                                 //  compute random for pixel from
      W1 = W1 * Frand[col][row1];                                                //    block randoms * weights
      W2 = W2 * Frand[col2][row1];
      W3 = W3 * Frand[col2][row];
      W4 = W4 * Frand[col2][row2];
      W5 = W5 * Frand[col][row2];
      W6 = W6 * Frand[col1][row2];
      W7 = W7 * Frand[col1][row];
      W8 = W8 * Frand[col1][row1];
      
      theta = W0 + W1 + W2 + W3 + W4 + W5 + W6 + W7 + W8;                        //  hue angle change
      theta = theta * 2.0 * Ampl - Ampl;                                         //  scale -Ampl ... +Ampl
   
      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      R1 = pix1[0];
      G1 = pix1[1];
      B1 = pix1[2];
      
      RGBtoHSL(R1,G1,B1,H,S,L);
      H = H + 180 * theta;                                                       //  displace H by -90 ... +90
      if (H < 0) H += 360.0;
      else if (H >= 360.0) H -= 360.0;
      HSLtoRGB(H,S,L,R3,G3,B3);

      if (sa_stat == 3 && edist < sa_blendwidth) {                               //  blend edges
         f1 = sa_blendfunc(edist);
         f2 = 1.0 - f1;
         R3 = f1 * R3 + f2 * R1;
         G3 = f1 * G3 + f2 * G1;
         B3 = f1 * B3 + f2 * B1;
      }

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

   pthread_exit(0);                                                              //  exit thread
}


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

//  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;
   int         E3ww, E3hh;
}


//  menu function

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

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

   cchar    *mess = E2X("Draw a line across the image in \n"
                        "direction of brightness change.");

   F1_help_topic = "brite ramp";

   EFbrite_ramp.menuname = menu;
   EFbrite_ramp.menufunc = m_brite_ramp;
   EFbrite_ramp.funcname = "brite_ramp";
   EFbrite_ramp.FprevReq = 1;                                                    //  use preview
   EFbrite_ramp.Fscript = 1;                                                     //  scripting supported                20.0
   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

   E3ww = E3pxm->ww;
   E3hh = E3pxm->hh;

   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] [Done] [Cancel]  |
         |_______________________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Brightness Ramp"),Mwin,Breset,Bdone,Bcancel,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",Ball,"space=5");
   zdialog_add_widget(zd,"radio","red","hbrgb",Bred,"space=3");
   zdialog_add_widget(zd,"radio","green","hbrgb",Bgreen,"space=3");
   zdialog_add_widget(zd,"radio","blue","hbrgb",Bblue,"space=3");

   GtkWidget *frameH = zdialog_widget(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, cchar *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();
      E3ww = E3pxm->ww;
      E3hh = E3pxm->hh;
      signal_thread();
      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
         E3ww = E3pxm->ww;
         E3hh = E3pxm->hh;
         signal_thread();
         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 (strmatch(event,"blendwidth")) Fupdate = 1;

   if (Fupdate) signal_thread();                                                 //  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))                                                    //  19.0
   {
      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 = 1;

   signal_thread();                                                              //  update image
   return;
}


//  this function is called when a curve is edited

void brite_ramp_curvedit(int spc)
{
   signal_thread();
   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 *);

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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      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,NWT);                                       //  worker threads

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

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

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


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       red1, green1, blue1, maxrgb;
   float       red3, green3, blue3;
   float       Fall, Fred, Fgreen, Fblue;
   float       dold, dnew;

   spldat *sd = EFbrite_ramp.sd;    

   for (py3 = index; py3 < E3hh; py3 += NWT)                                     //  loop output pixels
   for (px3 = 0; px3 < E3ww; px3++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py3 * E3ww + 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
      red1 = pix1[0];
      green1 = pix1[1];
      blue1 = pix1[2];
      
      red3 = red1 * Fall;                                                        //  curve "all" adjustment
      green3 = green1 * Fall;                                                    //    projected on each RGB color
      blue3 = blue1 * Fall;

      red3 = red3 * Fred;                                                        //  add additional RGB adjustments
      green3 = green3 * Fgreen;
      blue3 = blue3 * Fblue;

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

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

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

   pthread_exit(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 = E3hh - 1;
      return;
   }

   if (A == 0) {                                                                 //  horizontal line
      ex1 = 0;
      ex2 = E3ww - 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(E3ww,0,rx,ry);
   if (rx > E3ww || 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(E3ww,E3hh,rx,ry);
   if (rx > E3ww || ry > E3hh) {
      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,E3hh,rx,ry);
   if (rx < 0 || ry > E3hh) {
      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;
}


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

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

namespace paint_transp_names
{
   int      mode = 1;                                                            //  1/2 = more/less transparency
   int      Fgrad = 1;                                                           //  gradual or instant transparency
   int      Mradius;                                                             //  mouse radius
   float    kernel[400][400];                                                    //  radius <= 199
   int      E3ww, E3hh;

   editfunc    EFpaintransp;
}


//  menu function

void m_paint_transp(GtkWidget *, cchar *)
{
   using namespace paint_transp_names;

   int   paint_transp_dialog_event(zdialog* zd, cchar *event);
   void  paint_transp_mousefunc();

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

   F1_help_topic = "paint transp";

   EFpaintransp.menufunc = m_paint_transp;
   EFpaintransp.funcname = "paint_transp";
   EFpaintransp.Farea = 2;                                                       //  select area OK
   EFpaintransp.mousefunc = paint_transp_mousefunc;                              //  mouse function
   if (! edit_setup(EFpaintransp)) return;                                       //  setup edit
   
   E3ww = E3pxm->ww;
   E3hh = E3pxm->hh;

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

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

   ***/

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

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

   zdialog_restore_inputs(zd);                                                   //  preload prior user inputs

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

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

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


//  dialog event and completion callback function

int paint_transp_dialog_event(zdialog *zd, cchar *event)
{
   using namespace paint_transp_names;

   void  paint_transp_mousefunc();

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

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

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

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

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

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

      radius = Mradius;

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

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


//  pixel paint mouse function

void paint_transp_mousefunc()
{
   using namespace paint_transp_names;

   void  paint_transp_dopixels(int px, int py);

   static int  pmxdown = 0, pmydown = 0;
   int         px, py;

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

      px = Mxclick;
      py = Myclick;
      paint_transp_dopixels(px,py);                                              //  do 1 block of pixels
   }

   else if (Mxdrag || Mydrag)                                                    //  drag in progress
   {
      if (Mbutton == 1) mode = 1;                                                //  left drag, paint
      if (Mbutton == 3) mode = 2;                                                //  right drag, erase

      px = Mxdrag;
      py = Mydrag;

      if (Mxdown != pmxdown || Mydown != pmydown) {                              //  new drag
         pmxdown = Mxdown;
         pmydown = Mydown;
      }

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

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

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

   return;
}


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

void paint_transp_dopixels(int px, int py)
{
   using namespace paint_transp_names;

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

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   radius = Mradius;

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

      if (qx < 0 || qx > E3ww-1) continue;
      if (qy < 0 || qy > E3hh-1) continue;

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

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

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

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

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

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

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

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

   draw_context_destroy(draw_context);
   return;
}


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

//  mirror an image horizontally or vertically

editfunc    EFmirror;

void m_mirror(GtkWidget *, cchar *menu)
{
   int mirror_dialog_event(zdialog *zd, cchar *event);

   F1_help_topic = "mirror";

   EFmirror.menuname = menu;
   EFmirror.menufunc = m_mirror;
   EFmirror.funcname = "mirror";
   EFmirror.Fscript = 1;                                                         //  allow scripting
   EFmirror.Frestart = 1;                                                        //  allow restart
   if (! edit_setup(EFmirror)) return;                                           //  setup edit

/***
          _____________________________
         |      Mirror Image           |
         |                             |
         | [] horizontal  [] vertical  |
         |                             |
         |             [Done] [Cancel] |
         |_____________________________|
         
***/

   zdialog *zd = zdialog_new(E2X("Mirror Image"),Mwin,Bdone,Bcancel,null);
   EFmirror.zd = zd;
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","horz","hb1",E2X("horizontal"),"space=5");
   zdialog_add_widget(zd,"check","vert","hb1",E2X("vertical"),"space=5");

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

   return;
}


//  dialog event and completion callback function

int mirror_dialog_event(zdialog *zd, cchar *event)
{
   int mirror_horz(void);
   int mirror_vert(void);
   
   int      nn;

   if (strmatch(event,"focus")) return 1;
   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);
      return 1;
   }
   
   if (strmatch(event,"apply")) {                                                //  from script
      zdialog_fetch(zd,"horz",nn);
      if (nn) mirror_horz();
      zdialog_fetch(zd,"vert",nn);
      if (nn) mirror_vert();
   }

   if (strmatch(event,"horz")) mirror_horz();
   if (strmatch(event,"vert")) mirror_vert();

   return 1;
}


int mirror_horz()
{
   int      px, py;
   float    *pix3, *pix9;
   int      nc = E3pxm->nc, pcc = nc * sizeof(float);

   E9pxm = PXM_copy(E3pxm);

   for (py = 0; py < E3pxm->hh; py++)
   for (px = 0; px < E3pxm->ww; px++)
   {
      pix3 = PXMpix(E3pxm,px,py);                                                //  image9 = mirrored image3
      pix9 = PXMpix(E9pxm,E3pxm->ww-1-px,py);
      memcpy(pix9,pix3,pcc);
   }

   PXM_free(E3pxm);                                                              //  image9 >> image3
   E3pxm = E9pxm;
   E9pxm = 0;

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


int mirror_vert()
{
   int      px, py;
   float    *pix3, *pix9;
   int      nc = E3pxm->nc, pcc = nc * sizeof(float);

   E9pxm = PXM_copy(E3pxm);

   for (py = 0; py < E3pxm->hh; py++)
   for (px = 0; px < E3pxm->ww; px++)
   {
      pix3 = PXMpix(E3pxm,px,py);                                                //  image9 = mirrored image3
      pix9 = PXMpix(E9pxm,px,E3pxm->hh-1-py);
      memcpy(pix9,pix3,pcc);
   }

   PXM_free(E3pxm);                                                              //  image9 >> image3
   E3pxm = E9pxm;
   E9pxm = 0;

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


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

//  process image using a custom kernel

namespace anykernel_names
{
   int         ww, hh;                                                           //  image dimensions
   int         kernsize = 5;                                                     //  kernel size, N x N
   float       fmpyer = 1.0;                                                     //  multiplier
   int         fadder = 0;                                                       //  adder
   int         kernel[15][15];                                                   //  up to 15 x 15
   editfunc    EFanykernel;
}


void m_anykernel(GtkWidget *, cchar *menu)                                       //  menu function
{
   using namespace anykernel_names;

   int    anykernel_make_dialog(void);
   void * anykernel_thread(void *);

   F1_help_topic = "custom kernel";

   EFanykernel.menuname = menu;
   EFanykernel.menufunc = m_anykernel;
   EFanykernel.funcname = "custom_kernel";
   EFanykernel.Farea = 2;                                                        //  select area usable
   EFanykernel.Fscript = 1;                                                      //  scripting supported
   EFanykernel.threadfunc = anykernel_thread;                                    //  thread function
   EFanykernel.Frestart = 1;                                                     //  allow restart

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

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

   anykernel_make_dialog();
   return;
}


//  build the input dialog with the requested array size

int anykernel_make_dialog()
{
   using namespace anykernel_names;

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

   zdialog  *zd;
   int      row, col;
   char     rowname[16], cellname[30];

/***
       __________________________________________
      |              Custom Kernel               |
      |                                          |
      |  Kernel size [___]                       |
      |                                          |
      |  [____] [____] [____] [____] [____] ...  |
      |  [____] [____] [____] [____] [____] ...  |
      |  [____] [____] [____] [____] [____] ...  |
      |  [____] [____] [____] [____] [____] ...  |
      |  [____] [____] [____] [____] [____] ...  |
      |  [____] [____] [____] [____] [____] ...  |
      |   ...    ...    ...    ...    ...        |
      |                                          |
      |  multiply [____]   add [____]            |
      |                                          |
      |  Data file  [Load] [Save]                |
      |                                          |
      |         [Reset] [Apply] [Done] [Cancel]  |
      |__________________________________________|

***/

   zd = zdialog_new(E2X("Custom Kernel"),Mwin,Breset,Bapply,Bdone,Bcancel,null);
   EFanykernel.zd = zd;

   zdialog_add_widget(zd,"hbox","hbkern","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labkern","hbkern",E2X("Kernel size"),"space=3");
   zdialog_add_widget(zd,"zspin","kernsize","hbkern","3|15|2|5","size=3");
   zdialog_stuff(zd,"kernsize",kernsize);

   for (row = 1; row <= kernsize; row++)                                         //  make entry cell matrix
   {
      snprintf(rowname,16,"row%02d",row);
      zdialog_add_widget(zd,"hbox",rowname,"dialog","space=5");

      for (col = 1; col <= kernsize; col++)
      {
         snprintf(cellname,30,"cell%02d%02d",col,row);
         zdialog_add_widget(zd,"zspin",cellname,rowname,"-99|+99|1|0","size=4|space=4");
      }
   }

   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=5");                     //  multiplier and adder
   zdialog_add_widget(zd,"label","labmul","hbf",E2X("multiply"),"space=3");
   zdialog_add_widget(zd,"zspin","fmul","hbf","0|99|0.01|1","size=5");
   zdialog_add_widget(zd,"label","space","hbf",0,"space=3");
   zdialog_add_widget(zd,"label","labadd","hbf",E2X("add"),"space=3");
   zdialog_add_widget(zd,"zspin","fadd","hbf","-999|+999|1|0","size=5");

   zdialog_add_widget(zd,"hbox","hbfile","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labfile","hbfile",E2X("Data file"),"space=3");
   zdialog_add_widget(zd,"button","load","hbfile",Bload,"space=3");
   zdialog_add_widget(zd,"button","save","hbfile",Bsave,"space=3");

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


//  dialog event and completion callback function

int anykernel_dialog_event(zdialog *zd, cchar *event)
{
   using namespace anykernel_names;

   int      row, col, err, nn, kernsize2;
   float    value;
   char     cellname[20];
   char     *filename, dirname[200];
   FILE     *fid;
   cchar    *mess = E2X("Load settings from file");
   
   if (strmatch(event,"focus")) return 1;
   if (strmatch(event,"apply")) zd->zstat = 2;                                   //  from script
   if (strmatch(event,"done")) zd->zstat = 3;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 4;                                  //  from f_open()

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

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

         for (row = 1; row <= kernsize; row++)                                   //  get kernel values from dialog
         for (col = 1; col <= kernsize; col++)
         {
            snprintf(cellname,20,"cell%02d%02d",col,row);
            zdialog_fetch(zd,cellname,value);
            kernel[row-1][col-1] = value;
         }
         
         zdialog_fetch(zd,"fmul",fmpyer);
         zdialog_fetch(zd,"fadd",fadder);

         signal_thread();                                                        //  start thread
         return 1;
      }

      if (zd->zstat == 3)                                                        //  done - commit edit 
      {
         func_save_last_widgets(zd,null,"custom_kernel");
         edit_done(0);
      }
      
      else edit_cancel(0);                                                       //  cancel - discard edit
      return 1;
   }

   if (strmatch(event,"kernsize"))                                               //  change kernel size
   {
      zdialog_fetch(zd,"kernsize",kernsize2);
      if (kernsize2 == kernsize) return 1;
      kernsize = kernsize2;
      zdialog_destroy(zd);
      anykernel_make_dialog();
      return 1;
   }

   if (strmatch(event,"save"))                                                   //  save kernel data to a file
      func_save_widgets(zd,null,"custom_kernel",null);

   if (strmatch(event,"load"))                                                   //  load kernel data from a file 
   {
      snprintf(dirname,200,"%s/%s",get_zhomedir(),CEF->funcname);                //  folder for data files
      filename = zgetfile(mess,MWIN,"file",dirname,0);                           //  open data file
      if (! filename) return 1;                                                  //  user cancel

      fid = fopen(filename,"r");                                                 //  open file
      if (! fid) {
         zmessageACK(Mwin,"%s \n %s",filename,strerror(errno));
         zfree(filename);
         return 1;
      }

      nn = fscanf(fid,"kernsize == %d",&kernsize2);                              //  read 1st record, "kernsize == N"
      fclose(fid);

      if (nn != 1) {
         zmessageACK(Mwin,"file format error: \n %s",filename);
         zfree(filename);
         return 1;
      }      

      if (kernsize2 != kernsize) {                                               //  change kernel size to match file
         zdialog_destroy(zd);
         kernsize = kernsize2;
         anykernel_make_dialog();
         zd = EFanykernel.zd;                                                    //  changed                            20.0
      }
      
      fid = fopen(filename,"r");                                                 //  re-open file and load kernel data
      err = func_load_widgets(zd,null,"custom_kernel",fid);
      if (err) zmessageACK(Mwin,"file format error: \n %s",filename);
      fclose(fid);
      
      zfree(filename);
   }

   return 1;
}


//  image anykernel thread function

void * anykernel_thread(void *)
{
   using namespace anykernel_names;

   void * anykernel_wthread(void *arg);

   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request
      
      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      E9pxm = PXM_copy(E3pxm);                                                   //  [apply] accumulates 

      do_wthreads(anykernel_wthread,NWT);                                        //  worker threads
      
      PXM_free(E9pxm);

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

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

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


void * anykernel_wthread(void *arg)                                              //  worker thread function
{
   using namespace anykernel_names;

   int      index = *((int *) arg);
   int      px, py, qx, qy;
   int      ii, rad, dist = 0;
   float    kval, red, green, blue;
   float    max, f1, f2;
   float    *pix1, *pix3, *pixN;

   rad = kernsize / 2;                                                           //  image margins, not processed

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

      red = green = blue = 0;

      for (qy = -rad; qy <= +rad; qy++)                                          //  loop pixels around (px,py) 
      for (qx = -rad; qx <= +rad; qx++)                                          //    mapped to kernel
      {
         pixN = PXMpix(E9pxm,px+qx,py+qy);                                       //  pixel
         kval = kernel[qy+rad][qx+rad];                                          //  kernel value for pixel
         red   += kval * pixN[0];                                                //  sum (kernel * pixel value) per RGB 
         green += kval * pixN[1];
         blue  += kval * pixN[2];
      }

      if (fmpyer != 1.0) {                                                       //  apply multiplier
         red = fmpyer * red;
         green = fmpyer * green;
         blue = fmpyer * blue;
      }
      
      if (fadder != 0) {                                                         //  apply adder
         red = red + fadder;
         green = green + fadder;
         blue = blue + fadder;
      }

      if (red < 0) red = 0;                                                      //  stop underflow
      if (green < 0) green = 0;
      if (blue < 0) blue = 0;

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

      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      pix3[0] = red;
      pix3[1] = green;
      pix3[2] = blue;
   }

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

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

   pthread_exit(0);
}


