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

   Fotoxx      edit photos and manage collections

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

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

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

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

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

   Fotoxx image edit - Effects (filters) menu functions

   m_colordep           reduce color depth from 16 ... 1 bits
   m_sketch             convert photo to simulated pencil sketch
   m_cartoon            convert image into a cartoon drawing
   m_linedraw           combine image and highlighted edge pixels
   m_colordraw          convert image into high-contrast color drawing
   m_gradblur           blur image areas inversely proportional to contrast
   m_emboss             convert image to simulated embossing (3D relief)
   m_tiles              convert image into square tiles with 3D edges
   m_dots               convert image into dot grid (Roy Lichtenstein effect)
   m_painting           convert image into a simulated painting
   m_vignette           highlight selected image region
   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_anykernel          transform an image using any custom kernel
   m_waves              distort an image with a wave pattern
   m_dirblur            directed 1-dimensional blur using the mouse
   m_blur_BG            blur background areas (not in selected areas)

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

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

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


//  image color-depth reduction

int      colordep_depth = 16;                                                    //  bits per RGB color

editfunc    EFcolordep;


void m_colordep(GtkWidget *, cchar *)
{
   int    colordep_dialog_event(zdialog *zd, cchar *event);
   void * colordep_thread(void *);

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

   F1_help_topic = "color_depth";

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

   zdialog *zd = zdialog_new(ZTX("Set Color Depth"),Mwin,Bdone,Bcancel,null);
   EFcolordep.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,"spin","colors","hb2","1|16|1|16","space=5");

   colordep_depth = 16;

   zdialog_resize(zd,250,0);
   zdialog_run(zd,colordep_dialog_event,"-10/20");                               //  run dialog, parallel

   return;
}


//  dialog event and completion callback function

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

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

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

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

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

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

   return 0;
}


//  image color depth thread function

void * colordep_thread(void *)
{
   int         ii, px, py, rgb, dist = 0;
   uint16      m1, m2, val1, val3;
   float       *pix1, *pix3;
   float       fmag, f1, f2;

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

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

      fmag = 65535.0 / m1;

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

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

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

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

            pix3[rgb] = val3 / 256;
         }
      }

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

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


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

//  sketch menu function - convert photo to simulated pencil 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 *)                                         //  overhauled
{
   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 = ZTX("Convert to Sketch");

   F1_help_topic = "pencil_sketch";

   EFsketch.menufunc = m_sketch;
   EFsketch.funcname = "sketch";                                                 //  function name
   EFsketch.Farea = 2;                                                           //  select area usable
   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",ZTX("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",ZTX("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",ZTX("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",ZTX("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,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

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

   if (strmatch(event,"Bweight")) {
      zdialog_fetch(zd,"Bweight",Bweight);                                       //  revised brightness weight
      signal_thread();                                                           //  trigger update thread
   }

   if (strmatch(event,"Brthresh")) {
      zdialog_fetch(zd,"Brthresh",Brthresh);                                     //  brightness threshold
      signal_thread();
   }

   if (strmatch(event,"Cweight")) {
      zdialog_fetch(zd,"Cweight",Cweight);                                       //  contrast weight
      signal_thread();
   }

   if (strmatch(event,"cliplev")) {                                              //  output clip level
      zdialog_fetch(zd,"cliplev",cliplev);
      cliplev = 255 - cliplev;                                                   //  scale is reversed
      signal_thread();
   }

   if (strstr(event,"algorithm")) {                                              //  algorithm to use
      zdialog_fetch(zd,"algorithm1",ii);
      if (ii == 1) algorithm = 1;
      else algorithm = 2;
      signal_thread();
   }

   if (strmatch(event,"fgcolor")) {                                              //  foreground color                   15.07
      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);
      signal_thread();
   }

   if (strmatch(event,"bgcolor")) {                                              //  background color                   15.07
      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

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

      CEF->Fmods++;                                                              //  image modified
      CEF->Fsaved = 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_blend) {
            pix1 = PXMpix(E1pxm,px,py);                                          //  input pixel (blend area)
            f1 = sa_blendfunc(dist);                                             //  16.08
            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                                      //  16.02

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 *)
{
   using namespace cartoon;

   F1_help_topic = "cartoon";

   EFcartoon.menufunc = m_cartoon;
   EFcartoon.funcname = "cartoon";
   EFcartoon.Farea = 2;                                                          //  select area usable
   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 |-+]  |
      |                             |
      |            [Done] [Cancel]  |
      |_____________________________|
      
***/

   zdialog *zd = zdialog_new("Cartoon",Mwin,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","Line Threshold");
   zdialog_add_widget(zd,"label","lab1","vb1","Line Width");
   zdialog_add_widget(zd,"label","lab2","vb1","Blur Radius");
   zdialog_add_widget(zd,"label","lab1","vb1","Kuwahara Depth");

   zdialog_add_widget(zd,"spin","line_threshold","vb2","0|1000|1|1000");
   zdialog_add_widget(zd,"spin","line_width","vb2","0|10|1|1");
   zdialog_add_widget(zd,"spin","blur_radius","vb2","0|10|1|1");
   zdialog_add_widget(zd,"spin","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 using dialog params
   signal_thread();
   return;
}


//  dialog event and completion callback function

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

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

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

   if (strmatch(event,"line_threshold")) {
      zdialog_fetch(zd,"line_threshold",line_threshold);                         //  get outline threshold 0-1000
      signal_thread();
   }

   if (strmatch(event,"line_width")) {
      zdialog_fetch(zd,"line_width",line_width);                                 //  get line width 0-10
      signal_thread();
   }

   if (strmatch(event,"blur_radius")) {
      zdialog_fetch(zd,"blur_radius",blur_radius);                               //  get blur radius 0-10
      signal_thread();
   }

   if (strmatch(event,"kuwahara_depth")) {
      zdialog_fetch(zd,"kuwahara_depth",kuwahara_depth);                         //  get kuwahara depth 0-10
      signal_thread();
   }

   if (strmatch(event,"blendwidth")) signal_thread();                            //  select area blend width changed

   return 1;
}


//  thread function to update image

void * cartoon::thread(void *)                                                   //  improved                           16.06
{
   using namespace cartoon;
   
   int      Fblur, Fkuwa, Fcon;
   
   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request
      
      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;
      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;
   }
   
   for (int ii = 0; ii < NWT; ii++)                                              //  start worker threads
      start_wthread(blur_wthread,&Nval[ii]);
   wait_wthreads();                                                              //  wait for completion

   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];                                               //  16.06
         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_blend) {                                     //  if select area is active,
         f1 = sa_blendfunc(dist);                                                //    blend changes over sa_blend      16.08
         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;
   }

   exit_wthread();
   return 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;

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

   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_blend) {                                     //  if select area is active,
         f1 = sa_blendfunc(dist);                                                //    blend changes over sa_blend      16.08
         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;
   }

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


//  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()
{
   paintlock(1);
   PXM_free(E3pxm);
   E3pxm = PXM_copy(E9pxm);

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

   paintlock(0);
}


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_blend) {                                     //  select area is active,
         f1 = sa_blendfunc(dist);                                                //    blend changes over sa_blend      16.08
         f2 = 1.0 - f1;
         pix3[0] = f1 * pix3[0] + f2 * pix1[0];
         pix3[1] = f1 * pix3[1] + f2 * pix1[1];
         pix3[2] = f1 * pix3[2] + f2 * pix1[2];
         continue;
      }
   }

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


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

//  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_linedraw(GtkWidget *, cchar *)
{
   using namespace linedraw_names;

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

   F1_help_topic = "line_drawing";

   EFlinedraw.menufunc = m_linedraw;
   EFlinedraw.funcname = "linedraw";
   EFlinedraw.Farea = 2;                                                         //  select area usable
   EFlinedraw.threadfunc = linedraw_thread;                                      //  thread function
   if (! edit_setup(EFlinedraw)) return;                                         //  setup edit

   zdialog *zd = zdialog_new(ZTX("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",ZTX("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,"-10/20");                               //  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,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

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

   if (strmatch(event,"olth")) {
      zdialog_fetch(zd,"olth",outline_thresh);                                   //  get outline threshold 0-100
      signal_thread();
   }

   if (strmatch(event,"olww")) {
      zdialog_fetch(zd,"olww",outline_width);                                    //  get outline width 0-100
      signal_thread();
   }

   if (strmatch(event,"imbr")) {
      zdialog_fetch(zd,"imbr",image_briteness);                                  //  get image brightness 0-100
      signal_thread();
   }

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

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

   if (strmatch(event,"undo")) edit_undo();
   if (strmatch(event,"redo")) edit_redo();
   if (strmatch(event,"blendwidth")) 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

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

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

      CEF->Fmods++;
      CEF->Fsaved = 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_blend) {                                     //  select area is active,
         dnew = sa_blendfunc(dist);                                              //    blend changes over sa_blend      16.08
         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;
   }

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


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

//  convert an image into a high-contrast solid color drawing
//    somewhat like an illustration or cartoon

namespace colordraw_names
{
   editfunc    EFcolordraw;
   int         ww, hh;                                                           //  image dimensions
   int         threshx;                                                          //  threshold adjustment
   float       darkx, britex;                                                    //  dark and bright level adjustments
}


void m_colordraw(GtkWidget *, cchar *)                                           //  menu function
{
   using namespace colordraw_names;

   int    colordraw_dialog_event(zdialog *zd, cchar *event);
   void * colordraw_thread(void *);

   F1_help_topic = "color_drawing";

   EFcolordraw.menufunc = m_colordraw;
   EFcolordraw.funcname = "colordraw";
   EFcolordraw.Farea = 2;                                                        //  select area usable
   EFcolordraw.Frestart = 1;                                                     //  allow restart
   EFcolordraw.threadfunc = colordraw_thread;                                    //  thread function

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

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

/**
       ____________________________________________
      |            Color Drawing                   |
      |                                            |
      |   Threshold   ===============[]=========== |
      |   Dark Areas  ==================[]======== |
      |  Bright Areas =========[]================= |
      |                                            |
      |                            [Done] [Cancel] |
      |____________________________________________|

**/

   zdialog *zd = zdialog_new(ZTX("Color Drawing"),Mwin,Bdone,Bcancel,null);
   EFcolordraw.zd = zd;
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"space=3|expand|homog");
   zdialog_add_widget(zd,"label","labthresh","vb1",Bthresh);
   zdialog_add_widget(zd,"label","labdark","vb1",ZTX("Dark Areas"));
   zdialog_add_widget(zd,"label","labbrite","vb1",ZTX("Bright Areas"));
   zdialog_add_widget(zd,"hscale","threshx","vb2","0|255|1|100");
   zdialog_add_widget(zd,"hscale","darkx","vb2","0|1.0|0.002|0.0");
   zdialog_add_widget(zd,"hscale","britex","vb2","0|1.0|0.002|1.0");

   zdialog_restore_inputs(zd);                                                   //  preload prior user inputs
   zdialog_fetch(zd,"threshx",threshx);
   zdialog_fetch(zd,"darkx",darkx);
   zdialog_fetch(zd,"britex",britex);

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

   signal_thread();
   return;
}


//  dialog event and completion callback function

int colordraw_dialog_event(zdialog *zd, cchar *event)
{
   using namespace colordraw_names;

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

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

   if (strmatch(event,"threshx")) {                                              //  dark/bright threshold adjustment
      zdialog_fetch(zd,"threshx",threshx);
      signal_thread();
   }

   if (strmatch(event,"darkx")) {                                                //  dark level adjustment
      zdialog_fetch(zd,"darkx",darkx);
      signal_thread();
   }

   if (strmatch(event,"britex")) {                                               //  bright level adjustment
      zdialog_fetch(zd,"britex",britex);
      signal_thread();
   }

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

   return 1;
}


//  image drawing2 thread function

void * colordraw_thread(void *)
{
   using namespace colordraw_names;

   void * colordraw_wthread(void *arg);

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

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

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

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


void * colordraw_wthread(void *arg)                                              //  worker thread function
{
   using namespace colordraw_names;

   int      index = *((int *) arg);
   int      px, py, ii, tt, dist = 0;
   float    red1, green1, blue1;
   float    red3, green3, blue3;
   float    f1, f2, max;
   float    *pix1, *pix3;

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

      tt = threshx;                                                              //  threshold adjustment

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

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

      ii = 0.333 * (red1 + green1 + blue1);                                      //  classify pixel as dark or bright

      if (ii >= tt) {                                                            //  bright
         max = red1 + 1;                                                         //  make brightest - downward adjustment
         if (green1 > max) max = green1;
         if (blue1 > max) max = blue1;
         f1 = 255.0 / max;
         red3 = red1 * f1;
         green3 = green1 * f1;
         blue3 = blue1 * f1;
         if (britex < 1.0) {
            f1 = britex;
            f2 = 1.0 - f1;
            red3 = f1 * red3 + f2 * red1;
            green3 = f1 * green3 + f2 * green1;
            blue3 = f1 * blue3 + f2 * blue1;
         }
      }
      else {                                                                     //  dark
         red3 = red1 * darkx;                                                    //  make black + upward adjustment
         green3 = green1 * darkx;
         blue3 = blue1 * darkx;
      }

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

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

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

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


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

//  Graduated blur of image pixels.
//  Blur radius is zero ... max for pixel contrast max ... zero.

namespace gradblur_names
{
   editfunc    EFgradblur;
   int         con_limit;
   float       blur_radius;
   uint8       *pixcon;
   VOL int     gradblur_cancel;                                                  //  GCC inconsistent

   int         ww, hh, cc;
   int         pixseq_done[122][122];                                            //  up to blur_radius = 60
   int         pixseq_angle[1000];
   int         pixseq_dx[13000], pixseq_dy[13000];
   int         pixseq_rad[13000];
   int         max1 = 999, max2 = 12999;                                         //  for later overflow check
}


//  return a contrast value 0.0 - 1.0 for two given pixels to compare

inline float pixcontrast(float *pix1, float *pix2)
{
   float    match;
   match = PIXMATCH(pix1,pix2);                                                  //  0..1 = zero..perfect match
   return 1.0 - match;                                                           //  1 = high contrast (black/white)
}


void m_gradblur(GtkWidget *, cchar *)
{
   using namespace gradblur_names;

   int    gradblur_dialog_event(zdialog *zd, cchar *event);
   void * gradblur_thread(void *);

   int      ii, px, py, dx, dy;
   float    *pix1, *pix2;
   float    contrast, maxcon;

   F1_help_topic = "graduated_blur";

   EFgradblur.menufunc = m_gradblur;
   EFgradblur.funcname = "gradblur";
   EFgradblur.Farea = 2;                                                         //  select area usable
   EFgradblur.threadfunc = gradblur_thread;                                      //  thread function
   if (! edit_setup(EFgradblur)) return;                                         //  setup edit

   gradblur_cancel = 0;

   ww = E3pxm->ww;                                                               //  pre-calculate pixel contrasts
   hh = E3pxm->hh;
   cc = ww * hh;
   pixcon = (uint8 *) zmalloc(cc);                                               //  pixel contrast map

   for (py = 1; py < hh-1; py++)                                                 //  loop interior pixels
   for (px = 1; px < ww-1; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);                                                //  this pixel in base image E1
      contrast = maxcon = 0.0;

      for (dx = px-1; dx <= px+1; dx++)                                          //  loop neighbor pixels
      for (dy = py-1; dy <= py+1; dy++)
      {
         pix2 = PXMpix(E1pxm,dx,dy);
         contrast = pixcontrast(pix1,pix2);                                      //  contrast, 0-1
         if (contrast > maxcon) maxcon = contrast;
      }

      ii = py * ww + px;                                                         //  ii maps to (px,py)
      pixcon[ii] = 255 * maxcon;                                                 //  pixel contrast, 0 to 255
   }

/***
          _____________________________
         |     Graduated Blur          |
         |                             |
         |  Contrast Limit [ 40 +-]    |
         |   Blur Radius   [ 16 +-]    |
         |                             |
         |  [apply] [done] [cancel]    |
         |_____________________________|

***/

   zdialog *zd = zdialog_new(ZTX("Graduated Blur"),Mwin,Bapply,Bdone,Bcancel,null);
   EFgradblur.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",ZTX("Contrast Limit"),"space=5");
   zdialog_add_widget(zd,"spin","con_limit","hb1","1|255|1|50");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","lab5","hb2",ZTX("Blur Radius"),"space=5");
   zdialog_add_widget(zd,"spin","blur_radius","hb2","1|50|1|15");

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

   return;
}


//  dialog event and completion callback function

int gradblur_dialog_event(zdialog * zd, cchar *event)
{
   using namespace gradblur_names;

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

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  apply
      {
         zd->zstat = 0;                                                          //  keep dialog active
         zdialog_fetch(zd,"con_limit",con_limit);                                //  get parameters
         zdialog_fetch(zd,"blur_radius",blur_radius);
         edit_undo();                                                            //  reset image
         signal_thread();                                                        //  trigger working thread
      }
      else if (zd->zstat == 2) {                                                 //  done
         edit_done(0);                                                           //  commit edit
         zfree(pixcon);
      }
      else {                                                                     //  cancel
         gradblur_cancel = 1;                                                    //  16.02
         edit_cancel(0);                                                         //  discard edit
         zfree(pixcon);
      }
   }

   return 1;
}


//  image gradblur thread function

void * gradblur_thread(void *)
{
   using namespace gradblur_names;

   void * gradblur_wthread(void *arg);

   int         dx, dy, adx, ady;
   int         ii, jj;
   float       rad1, rad2, angle, astep;

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

      rad1 = blur_radius;

      for (dy = 0; dy <= 2*rad1; dy++)                                           //  no pixels mapped yet
      for (dx = 0; dx <= 2*rad1; dx++)
         pixseq_done[dx][dy] = 0;

      ii = jj = 0;

      astep = 0.5 / rad1;                                                        //  0.5 pixel steps at rad1 from center

      for (angle = 0; angle < 2*PI; angle += astep)                              //  loop full circle
      {
         pixseq_angle[ii] = jj;                                                  //  start pixel sequence for this angle
         ii++;

         for (rad2 = 1; rad2 <= rad1; rad2++)                                    //  loop rad2 from center to edge
         {
            dx = lround(rad2 * cos(angle));                                      //  pixel at angle and rad2
            dy = lround(rad2 * sin(angle));
            adx = rad1 + dx;
            ady = rad1 + dy;
            if (pixseq_done[adx][ady]) continue;                                 //  pixel already mapped
            pixseq_done[adx][ady] = 1;                                           //  map pixel
            pixseq_dx[jj] = dx;                                                  //  save pixel sequence for angle
            pixseq_dy[jj] = dy;
            pixseq_rad[jj] = rad2;                                               //  pixel radius
            jj++;
         }
         pixseq_rad[jj] = 9999;                                                  //  mark end of pixels for angle
         jj++;
      }

      pixseq_angle[ii] = 9999;                                                   //  mark end of angle steps

      if (ii > max1 || jj > max2)                                                //  should not happen
         zappcrash("gradblur array overflow");

      if (sa_stat == 3) Fbusy_goal = sa_Npixel;                                  //  setup progress tracking
      else  Fbusy_goal = ww * hh;
      Fbusy_done = 0;

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

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

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


void * gradblur_wthread(void *arg)                                               //  worker thread function
{
   using namespace gradblur_names;

   int      index = *((int *) arg);
   int      ii, jj, npix, dist = 0;
   int      px, py, dx, dy;
   int      ww = E3pxm->ww, hh = E3pxm->hh;
   float    red, green, blue, f1, f2;
   float    *pix1, *pix3, *pixN;
   int      rad, blurrad, con;

   for (py = index+1; py < hh-1; py += NWT)                                      //  loop interior pixels
   for (px = 1; px < ww-1; px++)
   {
      if (gradblur_cancel) exit_wthread();

      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] > con_limit) continue;                                      //  high contrast pixel

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

      red = pix1[0];                                                             //  blur center pixel
      green = pix1[1];
      blue = pix1[2];
      npix = 1;

      blurrad = 1.0 + blur_radius * (con_limit - pixcon[ii]) / con_limit;        //  blur radius for pixel, 1 - blur_radius

      for (ii = 0; ii < 2000; ii++)                                              //  loop angle around center pixel
      {
         jj = pixseq_angle[ii];                                                  //  1st pixel for angle step ii
         if (jj == 9999) break;                                                  //  none, end of angle loop

         while (true)                                                            //  loop pixels from center to radius
         {
            rad = pixseq_rad[jj];                                                //  next pixel step radius
            if (rad > blurrad) break;                                            //  stop here if beyond blur radius
            dx = pixseq_dx[jj];                                                  //  next pixel step px/py
            dy = pixseq_dy[jj];
            if (px+dx < 0 || px+dx > ww-1) break;                                //  stop here if off the edge
            if (py+dy < 0 || py+dy > hh-1) break;

            pixN = PXMpix(E1pxm,px+dx,py+dy);
            con = 255 * pixcontrast(pix1,pixN);
            if (con > con_limit) break;

            red += pixN[0];                                                      //  add pixel RGB values
            green += pixN[1];
            blue += pixN[2];
            npix++;
            jj++;
         }
      }

      pix3[0] = red / npix;                                                      //  average pixel values
      pix3[1] = green / npix;
      pix3[2] = blue / npix;

      if (sa_stat == 3 && dist < sa_blend) {
         f1 = sa_blendfunc(dist);                                                //  16.08
         f2 = 1.0 - f1;
         pix3[0] = f1 * pix3[0] + f2 * pix1[0];
         pix3[1] = f1 * pix3[1] + f2 * pix1[1];
         pix3[2] = f1 * pix3[2] + f2 * pix1[2];
      }

      Fbusy_done++;                                                              //  track progress
   }

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


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

//  convert image to simulate an embossing

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

editfunc    EFemboss;


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

   cchar  *title = ZTX("Simulate Embossing");

   F1_help_topic = "embossing";

   EFemboss.menufunc = m_emboss;
   EFemboss.funcname = "emboss";
   EFemboss.Farea = 2;                                                           //  select area usable
   EFemboss.threadfunc = emboss_thread;                                          //  thread function

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

   zdialog *zd = zdialog_new(title,Mwin,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,"spin","radius","hb1","0|9|1|0");
   zdialog_add_widget(zd,"label","lab2","hb1",ZTX("depth"),"space=5");
   zdialog_add_widget(zd,"spin","depth","hb1","0|99|1|0");
   zdialog_add_widget(zd,"check","color","hb1",Bcolor,"space=8");

   zdialog_run(zd,emboss_dialog_event,"-10/20");                                 //  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,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

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

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

   if (strmatchV(event,"radius","depth","color",null))
   {
      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
   }

   return 1;
}


//  thread function - use multiple working threads

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

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

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

      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

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

      CEF->Fmods++;
      CEF->Fsaved = 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);

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


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;                            //  15.09
            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_blend) {                               //  select area is active,
               dnew = sa_blendfunc(dist);                                        //    blend changes over sa_blend      16.08
               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;                               //  15.09
         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_blend) {                                     //  select area is active,
         dnew = sa_blendfunc(dist);                                              //    blend changes over sa_blend      16.08
         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 *)
{
   int    tiles_dialog_event(zdialog *zd, cchar *event);
   void * tiles_thread(void *);

   F1_help_topic = "tiles";

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

   zdialog *zd = zdialog_new(ZTX("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",ZTX("tile size"),"space=5");
   zdialog_add_widget(zd,"spin","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",ZTX("tile gap"),"space=5");
   zdialog_add_widget(zd,"spin","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",ZTX("3D depth"),"space=5");
   zdialog_add_widget(zd,"spin","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,"-10/20");                                  //  run dialog, parallel
   return;
}


//  dialog event and completion callback function

int tiles_dialog_event(zdialog * zd, cchar *event)
{
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 2;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel                    15.07

   if (zd->zstat && zd->zstat != 1)                                              //  dialog complete
   {
      if (zd->zstat == 2) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      zfree(tile_pixmap);
      return 1;
   }

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

   if (zd->zstat != 1) return 0;                                                 //  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
      return 1;
   }

   signal_thread();                                                              //  trigger working thread
   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

      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_blend) {                                              //  blend changes over sa_blend
               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);                                        //  16.08
               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;
      Fpaint2();                                                                 //  update window
   }

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


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

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

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


void m_dots(GtkWidget *, cchar *)
{
   int    dots_dialog_event(zdialog *zd, cchar *event);
   void * dots_thread(void *);

   F1_help_topic = "dot_matrix";

   EFdots.menufunc = m_dots;
   EFdots.funcname = "dots";
   EFdots.Farea = 2;                                                             //  select area usable
   EFdots.threadfunc = dots_thread;                                              //  thread function
   if (! edit_setup(EFdots)) return;                                             //  setup edit

   zdialog *zd = zdialog_new(ZTX("Convert Image to Dots"),Mwin,Bdone,Bcancel,null);
   EFdots.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labt","hb1",ZTX("dot size"),"space=5");
   zdialog_add_widget(zd,"spin","size","hb1","4|99|1|9","space=5");
   zdialog_add_widget(zd,"button","apply","hb1",Bapply,"space=10");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");

   dot_size = 9;

   zdialog_run(zd,dots_dialog_event,"-10/20");                                   //  run dialog, parallel
   return;
}


//  dialog event and completion callback function

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) 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,"apply")) return 0;                                      //  wait for [apply]

   zdialog_fetch(zd,"size",dot_size);                                            //  get dot size
   signal_thread();                                                              //  trigger working thread
   return 1;
}


//  image dots thread function

void * dots_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, qdist2;
   int         nc = E3pxm->nc;

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

      paintlock(1);                                                              //  block window updates               16.02

      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                  15.09
         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

            radius = ds * sqrt(fpi * relmax);                                    //  radius of dot with maximized color
            radius = radius * 0.875;                                             //  deliberate reduction
            if (radius < 0.5) continue;

            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;
               qdist2 = (qcx-dcx)*(qcx-dcx) + (qcy-dcy)*(qcy-dcy);               //  pixel distance**2 from center of dot
               if (qdist2 > radius2a) f1 = 0.0;                                  //  pixel outside dot, no color
               else if (qdist2 < radius2b) f1 = 1.0;                             //  pixel inside dot, full color
               else f1 = 1.0 - sqrt(qdist2) + radius - 0.5;                      //  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_blend) {
               f1 = sa_blendfunc(dist);                                          //  pixel in blend region              16.08
               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;                                                          //  15.09
            pix3 += nc;
         }
      }

      paintlock(0);                                                              //  unblock window updates             16.02

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

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


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

//  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 *)
{
   using namespace painting_names;

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

   F1_help_topic = "painting";

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

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

   zdialog_add_widget(zd,"hbox","hbcd","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","lab1","hbcd",ZTX("color depth"),"space=5");
   zdialog_add_widget(zd,"spin","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",ZTX("patch area goal"),"space=5");
   zdialog_add_widget(zd,"spin","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",ZTX("req. color match"),"space=5");
   zdialog_add_widget(zd,"spin","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",ZTX("borders"),"space=5");
   zdialog_add_widget(zd,"check","borders","hbbd",0,"space=2");
   zdialog_add_widget(zd,"button","apply","hbbd",Bapply,"space=10");

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

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

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

   if (strmatch(event,"apply")) {                                                //  apply user settings
      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
   }

   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

      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;
      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_blend > 0)
   {
      for (ii = 0; ii < E3pxm->ww * E3pxm->hh; ii++)                         //  find pixels in select area
      {
         dist = sa_pixmap[ii];
         if (! dist || dist >= sa_blend) 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_blend     16.08
         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;
}


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

   Vignette function

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

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

void  vign_mousefunc();

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


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

   F1_help_topic = "vignette";

   cchar    *title = ZTX("Vignette");

   EFvignette.menufunc = m_vignette;
   EFvignette.funcname = "vignette";
   EFvignette.Farea = 2;                                                         //  select area usable
   EFvignette.FprevReq = 1;                                                      //  use preview image
   EFvignette.threadfunc = Vign_thread;                                          //  thread function
   EFvignette.mousefunc = vign_mousefunc;                                        //  mouse function
   if (! edit_setup(EFvignette)) return;                                         //  setup edit

/***
            _____________________________
           |                             |
           |                             |
           |    curve drawing area       |
           |                             |
           |_____________________________|
            center                   edge

            (o) Brightness  (o) Color [___]
            Curve File: [ Open ] [ Save ]

                          [Done] [Cancel]

***/

   zdialog *zd = zdialog_new(title,Mwin,Bdone,Bcancel,null);
   EFvignette.zd = zd;

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

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

   zdialog_add_widget(zd,"hbox","hb3","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labcurve","hb3",Bcurvefile,"space=5");
   zdialog_add_widget(zd,"button","load","hb3",Bopen,"space=5");
   zdialog_add_widget(zd,"button","savecurve","hb3",Bsave,"space=5");

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

   vign_cx = E3pxm->ww / 2;                                                      //  initial vignette center
   vign_cy = E3pxm->hh / 2;

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

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

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

   sd->Nspc = 2;                                                                 //  2 curves

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

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

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

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

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


//  dialog event and completion callback function

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

   spldat   *sd = EFvignette.curves;
   int      ii;
   char     color[20];
   cchar    *pp;

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

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

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

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

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

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

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

   if (strmatch(event,"load")) {                                                 //  load saved curve
      splcurve_load(sd);
      Vign_curvedit(0);
      signal_thread();
      return 0;
   }

   if (strmatch(event,"savecurve")) {                                            //  save curve to file
      splcurve_save(sd);
      return 0;
   }

   return 0;
}


//  get mouse position and set new center for vignette

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

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

   Mxdrag = Mydrag = 0;

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


//  this function is called when the curve is edited

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


//  thread function

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

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

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

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

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


//  working thread

void * Vign_wthread(void *arg)
{
   float       *pix1, *pix3;
   int         index, ii, kk, px, py, dist = 0;
   float       cx, cy, rad, radx, rady, f1, f2, xval, yval;
   float       red1, green1, blue1, red3, green3, blue3, cmax;
   spldat      *sd = EFvignette.curves;

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

   index = *((int *) arg);

   for (py = index; py < E3pxm->hh; py += NWT)                                   //  loop all image pixels
   for (px = 0; px < E3pxm->ww; px++)
   {
      ii = py * E3pxm->ww + px;

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

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

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

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

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

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

      red3 = yval * red1;                                                        //  adjust brightness
      green3 = yval * green1;
      blue3 = yval * blue1;

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

      red3 = f1 * vignette_RGB[0] + f2 * red3;                                   //  mix input and vignette color
      green3 = f1 * vignette_RGB[1] + f2 * green3;
      blue3 = f1 * vignette_RGB[2] + f2 * blue3;

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

      cmax = red3;                                                               //  detect overflow
      if (green3 > cmax) cmax = green3;
      if (blue3 > cmax) cmax = blue3;

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

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

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


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

//  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 *, const char *) 
{
   using namespace texturenames;

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

   zdialog  *zd;

   F1_help_topic = "texture";

   EFtexture.menufunc = m_texture;
   EFtexture.funcname = "texture";                                               //  function name
   EFtexture.Farea = 2;                                                          //  select area OK
   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(ZTX("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,"spin","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,"spin","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,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 2;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel                    15.07

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

   if (strmatch(event,"radius"))                                                 //  get radius
      zdialog_fetch(zd,"radius",radius);

   if (strmatch(event,"power"))                                                  //  get power
      zdialog_fetch(zd,"power",power);

   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

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

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

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

      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_blend) {                                     //  select area is active,
         f3 = sa_blendfunc(dist);                                                //    blend changes over sa_blend      16.08
         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++;
   }

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


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

//  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         update_thread = 0;
}

void m_pattern(GtkWidget *, const char *)
{
   using namespace patternnames;

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

   zdialog  *zd;

   F1_help_topic = "pattern";

   EFpattern.menufunc = m_pattern;
   EFpattern.funcname = "pattern";                                               //  function name
   EFpattern.Farea = 2;                                                          //  select area OK
   EFpattern.threadfunc = pattern_thread;                                        //  thread function

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

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

/***
       _______________________________________________
      | [x][-][_]  Background Pattern                 |
      |                                               |
      |  Pattern File [____________________] [Browse] |
      |  Geometry [Calculate]   Zoom [___|-+]         |
      |  Pattern Width [___|-+]  Height [___|-+]      |
      |  Overlap Width [___|-+]  Height [___|-+]      |
      |  Opacity [___|-+]  Contrast [___|-+]          |
      |                                               |
      |                               [done] [cancel] |
      |_______________________________________________|

***/

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

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

   zdialog_add_widget(zd,"hbox","hbcalc","dialog");
   zdialog_add_widget(zd,"label","labzoom","hbcalc",ZTX("Zoom"),"space=5");
   zdialog_add_widget(zd,"spin","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",ZTX("Geometry"),"space=5");
   zdialog_add_widget(zd,"button","calc","hbcalc",ZTX("Calculate"));

   zdialog_add_widget(zd,"hbox","hbsize","dialog");
   zdialog_add_widget(zd,"vbox","vbs1","hbsize",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vbs2","hbsize",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","space","hbsize",0,"space=8");
   zdialog_add_widget(zd,"vbox","vbs3","hbsize",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vbs4","hbsize",0,"space=3|homog");

   zdialog_add_widget(zd,"hbox","hbs11","vbs1");
   zdialog_add_widget(zd,"label","labpatt","hbs11",ZTX("Pattern"),"space=3");
   zdialog_add_widget(zd,"label","labwidth","hbs11",Bwidth,"space=3");
   zdialog_add_widget(zd,"spin","pattww","vbs2","20|1000|1|100");
   zdialog_add_widget(zd,"label","labheight","vbs3",Bheight,"space=3");
   zdialog_add_widget(zd,"spin","patthh","vbs4","20|1000|1|100");

   zdialog_add_widget(zd,"hbox","hbs12","vbs1");
   zdialog_add_widget(zd,"label","labover","hbs12",ZTX("Overlap"),"space=3");
   zdialog_add_widget(zd,"label","labwidth","hbs12",Bwidth,"space=3");
   zdialog_add_widget(zd,"spin","olapww","vbs2","0|1000|1|0");
   zdialog_add_widget(zd,"label","labheight","vbs3",Bheight,"space=3");
   zdialog_add_widget(zd,"spin","olaphh","vbs4","0|1000|1|0");

   zdialog_add_widget(zd,"hbox","hbopac","dialog");
   zdialog_add_widget(zd,"label","labopac","hbopac",ZTX("Opacity"),"space=5");
   zdialog_add_widget(zd,"spin","opacity","hbopac","0|100|1|0");
   zdialog_add_widget(zd,"label","space","hbopac",0,"space=10");
   zdialog_add_widget(zd,"label","labcont","hbopac",ZTX("Contrast"),"space=5");
   zdialog_add_widget(zd,"spin","contrast","hbopac","0|200|1|100");

   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    15.11
{
   using namespace patternnames;

   void  pattern_match();

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

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      if (pixbuf) g_object_unref(pixbuf);                                        //  free memory
      pixbuf = 0;
      if (pattfile) zfree(pattfile);
      pattfile = 0;
      return 1;
   }
   
   zdialog_fetch(zd,"pattww",pattww);                                            //  fetch all dialog parameters
   zdialog_fetch(zd,"patthh",patthh);
   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_dirk,temp1);
         pattfile = zstrdup(temp2);
      }
   }

   if (strmatch(event,"browse"))                                                 //  choose new pattern file
   {
      if (pattfile) pp = pattfile;
      else pp = pattern_dirk;
      file = zgetfile(ZTX("choose pattern tile"),MWIN,"file",pp);
      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 (strstr("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_dirk))                                       //  add pattern to directory if needed
         shell_quiet("cp \"%s\" \"%s\" ",pattfile,pattern_dirk);

      pixbuf2 = gdk_pixbuf_stripalpha(pixbuf);                                   //  strip alpha channel if present     15.08
      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

   if (strmatch(event,"browse")) {
      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 (strstr("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     15.08
      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);          //  rescale pixbuf
      if (! pixbuf2) return 1;

      g_object_unref(pixbuf);                                                    //  replace original pixbuf
      pixbuf = pixbuf2;

      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 (strmatch(event,"init")) {
         zdialog_fetch(zd,"pattww",pattww);                                      //  leave unchanged if "init"
         zdialog_fetch(zd,"patthh",patthh);
      }
   }
   
   if (olapww > pattww/3) olapww = pattww/3;                                     //  prevent nonsense                   15.11
   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);

   if (strstr("init browse calc zoom pattww patthh olapww olaphh opacity contrast",event)) {
      update_thread++;
      signal_thread();                                                           //  update image
   }

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


//  thread function - update image

void * pattern_thread(void *)
{
   using namespace patternnames;

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

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

      if (! pixbuf) continue;

      while (update_thread) {
         update_thread = 0;
         start_wthread(pattern_wthread,&Nval[0]);                                //  start worker thread (1)
         wait_wthreads();                                                        //  wait for completion
      }

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

      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop warning
}


void * pattern_wthread(void *arg)                                                //  worker thread function
{
   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;
   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                15.11
   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
         pix3 = PXMpix(E3pxm,mpx,mpy);                                           //  output image pixel
         pix3[0] += (0.01 * opacity / tmap[ii]) * pixt[0];                       //  image = tile * opacity 0-100%
         pix3[1] += (0.01 * opacity / tmap[ii]) * pixt[1];                       //  reduce for overlapping tiles
         pix3[2] += (0.01 * opacity / tmap[ii]) * pixt[2];
      }
   }

   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            15.11
   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                 15.11
   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_blend)
               f1 = sa_blendfunc(dist);                                          //  blend edges                        16.08
            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];
      }
   }

   zfree(tmap);
   zfree(tbmap);
   exit_wthread();                                                               //  exit thread
   return 0;                                                                     //  not executed, avoid gcc warning
}


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

//  Create a mosaic image using tiny thumbnails as tiles.

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
   int      Nfiles, Ntiles;                  //  tile file count, tile image count
   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)

   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
   char     tilesavefile[200];

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

   char  **fif_filelist;                     //  find_imagefiles() variables
   int   fif_count1;
   int   fif_count2;
}


//  menu function

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

   cchar    *title = ZTX("Create Mosaic");
   zdialog  *zd;
   int      ii, cc, nfid, cc1, cc2;
   char     label[12];

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

   snprintf(tilesavefile,200,"%s/mosaic_tiles",get_zuserdir());                  //  read tile images from save file

   nfid = open(tilesavefile,O_RDONLY);
   if (nfid >= 0)
   {
      cc1 = sizeof(int);
      cc2 = read(nfid,&Ntiles,cc1);                                              //  read tile count, width, height
      cc2 = read(nfid,&tww,cc1);
      cc2 = read(nfid,&thh,cc1);

      for (ii = 0; ii < Ntiles; ii++)                                            //  read tile images
      {
         cc2 = read(nfid,&cc1,sizeof(int));
         tilefile[ii] = (char *) zmalloc(cc1);
         cc2 = read(nfid,tilefile[ii],cc1);                                      //  tile filespec
         if (cc2 != cc1) break;
         cc1 = tww * thh * 3;
         tileimage[ii] = (uint8 *) zmalloc(cc1);
         cc2 = read(nfid,tileimage[ii],cc1);                                     //  tile image
         if (cc2 != cc1) break;
         cc1 = 3 * sizeof(float);
         cc2 = read(nfid,&tileRGB1[ii],cc1);                                     //  tile RGB, each quadrant
         cc2 = read(nfid,&tileRGB2[ii],cc1);
         cc2 = read(nfid,&tileRGB3[ii],cc1);
         cc2 = read(nfid,&tileRGB4[ii],cc1);
         if (cc2 != cc1) break;
      }

      close(nfid);

      if (cc2 != cc1) {                                                          //  file error
         zmessageACK(Mwin,"cannot read tile save file");
         tww = 32;                                                               //  reset default tile size
         thh = 24;
         Nfiles = Ntiles = 0;                                                    //  tile file and image counts
      }
   }


/***
       _______________________________________________
      |               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",ZTX("Tile"),"space=3");
   zdialog_add_widget(zd,"label","labwidth","hbsize",Bwidth,"space=3");
   zdialog_add_widget(zd,"spin","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,"spin","height","hbsize","16|48|2|24");

   zdialog_add_widget(zd,"hbox","hbcreate","dialog");
   zdialog_add_widget(zd,"button","tiles","hbcreate",ZTX("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",ZTX("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_names::mosaic_dialog_event(zdialog *zd, const char *event)
{
   using namespace mosaic_names;

   PIXBUF         *pxb1, *pxb2;
   GError         *gerror = 0;
   static char    **flist = 0;
   char           label[12];
   int            ii, jj, err, NF;
   int            trow, tcol, tpx, tpy, ipx, ipy;
   int            tww1, thh1, tww2, thh2;
   int            rs, bestii, blend;
   uint8          *tpixels, *tpix, *timage;
   uint8          *row1, *row2;
   float          *pix1, *pix3;

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

   float          match, bestmatch;
   float          f1, f3, coeff;
   int            nfid, cc1, cc2;

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

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

   if (strmatch(event,"focus"))
      takeMouse(mosaic_mousefunc,dragcursor);                                    //  connect mouse function

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

      for (ii = 0; ii < Ntiles; ii++)
         zfree(tileimage[ii]);

      Nfiles = Ntiles = 0;

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

      err = find_imagefiles(thumbdirk,flist,NF,1);                               //  find all thumbnail image files
      if (err) {
         zmessageACK(Mwin,strerror(errno));
         return 1;
      }

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

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

      Nfiles = NF;

      zfree(flist);
      flist = 0;

      if (Nfiles < 100) {
         zmessageACK(Mwin,ZTX("only %d tile images found"),Nfiles);
         return 1;
      }

      for (ii = 0; ii < Ntiles; ii++)                                            //  free tile image memory
         zfree(tileimage[ii]);

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

      for (Ntiles = ii = 0; ii < Nfiles; ii++)                                   //  loop tile image files
      {
         pxb1 = gdk_pixbuf_new_from_file_at_size                                 //  create pixbuf fitting within 64x64
                                 (tilefile[ii],64,64,&gerror);
         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 * 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                                 //  rescale for max. tww x thh section
                              (tilefile[ii],tww2,thh2,&gerror);                  //    (tww2 = tww and thh2 >= thh
         if (! pxb2) {                                                           //      or thh2 = thh and tww2 >= tww)
            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 (jj = 0; jj < thh; jj++)
         {                                                                       //  copy pixbuf tww x thh section
            row1 = tpixels + rs * (tpy + jj) + tpx * 3;
            row2 = timage + tww * 3 * jj;
            memcpy(row2,row1,tww * 3);
         }

         g_object_unref(pxb2);

         tileimage[Ntiles] = timage;                                             //  save final tile image
         Ntiles++;

         snprintf(label,12,"%d",Ntiles);                                         //  show count in dialog
         zdialog_stuff(zd,"labNtiles",label);
         zmainloop();
      }

      for (ii = 0; ii < Ntiles; ii++)                                            //  loop tile images
      {
         timage = tileimage[ii];

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

      nfid = open(tilesavefile,O_WRONLY|O_CREAT|O_TRUNC,0640);                   //  save tile images to file
      if (nfid < 0) {
         zmessageACK(Mwin,"%s \n %s",tilesavefile,strerror(errno));
         return 1;
      }

      cc1 = sizeof(int);                                                         //  write tile count
      cc2 = write(nfid,&Ntiles,cc1);
      cc2 = write(nfid,&tww,cc1);
      cc2 = write(nfid,&thh,cc1);

      for (ii = 0; ii < Ntiles; ii++)                                            //  write tile images
      {
         cc1 = strlen(tilefile[ii]) + 1;
         cc2 = write(nfid,&cc1,sizeof(int));
         cc2 = write(nfid,tilefile[ii],cc1);                                     //  tile file + trailing 0
         if (cc2 != cc1) break;
         cc1 = tww * thh * 3;
         cc2 = write(nfid,tileimage[ii],cc1);                                    //  tile image
         if (cc2 != cc1) break;
         cc1 = 3 * sizeof(float);
         cc2 = write(nfid,&tileRGB1[ii],cc1);                                    //  tile RGB, each quadrant
         cc2 = write(nfid,&tileRGB2[ii],cc1);
         cc2 = write(nfid,&tileRGB3[ii],cc1);
         cc2 = write(nfid,&tileRGB4[ii],cc1);
         if (cc2 != cc1) break;
      }

      close(nfid);

      if (cc2 != cc1)
         zmessageACK(Mwin,"cannot write tile save file");
   }

   if (strmatch(event,"image"))
   {
      if (! Ntiles) return 1;

      edit_undo();                                                               //  reset to original image

      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++)
      {
         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
         {
            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] = 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];
            ii = iww * ipy + ipx;                                                //  save map of tile used 
            tilemap[ii] = bestii;                                                //    at each image pixel
         }

         Fpaint2();                                                              //  update image for each tile
         zmainloop();                                                            //  (actually periodic)
      }

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

   return 1;
}


//  mouse function - called for mouse events inside image

void mosaic_names::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;
}


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

//  process image using a custom kernel

namespace anykernel_names
{
   zdialog     *zd = 0;
   int         ww, hh;                                                           //  image dimensions
   int         kernsize = 5;                                                     //  kernel size, N x N
   int         divisor = 1;                                                      //  kernel divisor
   int         kernel[15][15];                                                   //  up to 15 x 15
   editfunc    EFanykernel;
}


void m_anykernel(GtkWidget *, cchar *)                                           //  menu function
{
   using namespace anykernel_names;

   int    anykernel_make_dialog();
   void * anykernel_thread(void *);

   F1_help_topic = "custom_kernel";

   EFanykernel.menufunc = m_anykernel;
   EFanykernel.funcname = "custom_kernel";
   EFanykernel.Farea = 2;                                                        //  select area usable
   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()                                                      //  15.10
{
   using namespace anykernel_names;

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

   int      row, col;
   char     rowname[12], cellname[12];

/***
       __________________________________________
      |              Custom Kernel               |
      |                                          |
      |  Kernel size [___|+-]   Divisor [____]   |
      |  Data file  [Load] [Save]                |
      |                                          |
      |  [____] [____] [____] [____] [____] ...  |
      |  [____] [____] [____] [____] [____] ...  |
      |  [____] [____] [____] [____] [____] ...  |
      |  [____] [____] [____] [____] [____] ...  |
      |  [____] [____] [____] [____] [____] ...  |
      |  [____] [____] [____] [____] [____] ...  |
      |  ...                                     |
      |                                          |
      |         [Reset] [Apply] [Done] [Cancel]  |
      |__________________________________________|

***/

   if (zd) zdialog_free(zd);

   zd = zdialog_new(ZTX("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",ZTX("Kernel size"),"space=3");
   zdialog_add_widget(zd,"spin","kernsize","hbkern","3|15|2|5");
   zdialog_add_widget(zd,"label","space","hbkern",0,"space=5");
   zdialog_add_widget(zd,"label","labdiv","hbkern",ZTX("Divisor"),"space=3");
   zdialog_add_widget(zd,"entry","divisor","hbkern","1","size=3");
   zdialog_add_widget(zd,"hbox","hbfile","dialog");
   zdialog_add_widget(zd,"label","labfile","hbfile",ZTX("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_add_widget(zd,"vbox","space","dialog",0,"space=5");

   for (row = 1; row <= kernsize; row++)
   {
      snprintf(rowname,12,"row%02d",row);
      zdialog_add_widget(zd,"hbox",rowname,"dialog","space=5");

      for (col = 1; col <= kernsize; col++)
      {
         snprintf(cellname,12,"cell%02d%02d",col,row);
         zdialog_add_widget(zd,"entry",cellname,rowname,"0","size=3|space=5");
      }
   }
   
   zdialog_stuff(zd,"kernsize",kernsize);                                        //  stuff prior values
   zdialog_stuff(zd,"divisor",divisor);

   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[12];
   char     *filename, dirname[200];
   FILE     *fid;
   cchar    *mess = ZTX("Load settings from file");

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

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  reset                              15.10
      {
         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,12,"cell%02d%02d",col,row);
            zdialog_fetch(zd,cellname,value);
            kernel[row-1][col-1] = value;
         }
         
         zdialog_fetch(zd,"divisor",divisor);
         signal_thread();                                                        //  start thread
         return 1;
      }

      if (zd->zstat == 3)                                                        //  done - commit edit 
      {
         edit_save_last_widgets(&EFanykernel);                                   //  15.10
         edit_done(0);
      }
      
      else edit_cancel(0);                                                       //  cancel - discard edit
      return 1;
   }

   if (strmatch(event,"kernsize"))                                               //  change kernel size
   {
      zdialog_fetch(zd,"kernsize",kernsize);
      zd->zstat = 3;
      anykernel_make_dialog();
      return 1;
   }

   if (strmatch(event,"save"))                                                   //  save kernel data to a file
      edit_save_widgets(&EFanykernel);

   if (strmatch(event,"load"))                                                   //  load kernel data from a file       15.10
   {
      snprintf(dirname,200,"%s/%s",get_zuserdir(),CEF->funcname);                //  directory 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
         kernsize = kernsize2;
         anykernel_make_dialog();
      }
      
      fid = fopen(filename,"r");                                                 //  re-open file and load kernel data
      err = edit_load_widgets(&EFanykernel,fid);
      if (err) zmessageACK(Mwin,"file format error: \n %s",filename);
      
      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
      
      E9pxm = PXM_copy(E3pxm);                                                   //  [apply] accumulates                15.10

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

      CEF->Fmods++;
      CEF->Fsaved = 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 (divisor) {                                                             //  divide sum by divisor
         f1 = 1.0 / divisor;
         red = f1 * red;
         green = f1 * green;
         blue = f1 * blue;
      }

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

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

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


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

//  make waves menu function
//  distort the image with a wave pattern
//  independent horizontal and vertical wavelengths
//  variance option: make waves more or less irregular
//  perspective option: wavelengths lengthen going down

namespace waves_names
{
   editfunc EFwaves;
   int      ww, hh;                          //  image dimensions
   int      WLV, WLH;                        //  vertical and horizontal wavelengths
   int      AMPV, AMPH;                      //  vertical and horizontal amplitudes
   int      VARV, VARH;                      //  vertical and horizontal variance
   int      PERSP;                           //  perspective 0-100
}


void m_waves(GtkWidget *, const char *)
{
   using namespace waves_names;

   int    waves_dialog_event(zdialog* zd, const char *event);
   void * waves_thread(void *);

   F1_help_topic = "make_waves";

   EFwaves.menufunc = m_waves;
   EFwaves.funcname = "make_waves";
   EFwaves.Farea = 2;                                                            //  select area usable
   EFwaves.threadfunc = waves_thread;

   if (! edit_setup(EFwaves)) return;

/***
          ___________________________________
         |           Make waves             |
         |                                   |
         |             Horizontal  Vertical  |
         |  wavelength [___|-+]    [___|-+]  |
         |  amplitude  [___|-+]    [___|-+]  |
         |  variance   [___|-+]    [___|-+]  |
         |                                   |
         |  perspective [___|-+]             |
         |                                   |
         |          [apply] [done] [cancel]  |
         |___________________________________|

***/

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

   zdialog *zd = zdialog_new(ZTX("Make Waves"),Mwin,Bapply,Bdone,Bcancel,null);
   EFwaves.zd = zd;
   zdialog_add_widget(zd,"hbox","hbw","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbw1","hbw",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbw2","hbw",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbw3","hbw",0,"space=3");
   zdialog_add_widget(zd,"label","space","vbw1"," ","space=1");
   zdialog_add_widget(zd,"label","labwl","vbw1",ZTX("wavelength"),"expand");
   zdialog_add_widget(zd,"label","labamp","vbw1",ZTX("amplitude"),"expand");
   zdialog_add_widget(zd,"label","labamp","vbw1",ZTX("variance"),"expand");
   zdialog_add_widget(zd,"label","labh","vbw2",ZTX("horizontal"),"space=1");
   zdialog_add_widget(zd,"spin","wlh","vbw2","3|500|1|50","expand");
   zdialog_add_widget(zd,"spin","amph","vbw2","0|100|1|20","expand");
   zdialog_add_widget(zd,"spin","varh","vbw2","0|100|1|20","expand");
   zdialog_add_widget(zd,"label","labh","vbw3",ZTX("vertical"),"space=1");
   zdialog_add_widget(zd,"spin","wlv","vbw3","3|500|1|50","expand");
   zdialog_add_widget(zd,"spin","ampv","vbw3","0|100|1|20","expand");
   zdialog_add_widget(zd,"spin","varv","vbw3","0|100|1|20","expand");
   zdialog_add_widget(zd,"hsep","sepp","dialog",0,"space=3");
   zdialog_add_widget(zd,"hbox","hbp","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labp","hbp",ZTX("perspective"),"space=3");
   zdialog_add_widget(zd,"spin","persp","hbp","0|100|1|0","space=5");

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


//  waves dialog event and completion function

int waves_dialog_event(zdialog *zd, const char *event)                           //  waves dialog event function
{
   using namespace waves_names;

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  apply
         zd->zstat = 0;                                                          //  keep dialog active
         edit_reset();
         zdialog_fetch(zd,"wlv",WLV);                                            //  get user inputs
         zdialog_fetch(zd,"wlh",WLH);
         zdialog_fetch(zd,"ampv",AMPV);
         zdialog_fetch(zd,"amph",AMPH);
         zdialog_fetch(zd,"varv",VARV);
         zdialog_fetch(zd,"varh",VARH);
         zdialog_fetch(zd,"persp",PERSP);
         signal_thread();                                                        //  calculate
         return 1;
      }
      if (zd->zstat == 2) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
   }

   return 1;
}


//  thread function - multiple working threads to update image

void * waves_thread(void *)
{
   using namespace waves_names;

   void * waves_wthread(void *);

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

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

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

   return 0;                                                                     //  not executed, stop warning
}


void * waves_wthread(void *arg)                                                  //  working threads
{
   using namespace waves_names;

   int      index = *((int *) (arg));
   float    wlv, wlh, ampv, amph, varv, varh, persp;
   int      py, px, pylo, pyhi;
   int      ii, edist = 0;
   float    dx, dy, *pix1, *pix3, vpix[4];
   float    f1, f2, ffv, ffh, red, green, blue;

   wlv = WLV;                                                                    //  vertical and horizontal wavelengths
   wlh = WLH;
   ampv = 0.01 * AMPV * wlv;                                                     //  vertical and horizoneal amplitudes
   amph = 0.01 * AMPH * wlh;
   varv = 0.01 * VARV;                                                           //  vertical and horizontal variance
   varh = 0.01 * VARH;
   persp = 0.01 * PERSP;                                                         //  perspective 0 to 1.0

   pylo = 0;                                                                     //  py range
   pyhi = hh;

   if (sa_stat == 3) {
      pylo = sa_miny - wlv;                                                      //  rescale to select area height
      if (pylo < 0) pylo = 0;
      pyhi = sa_maxy + wlv;
      if (pyhi > hh) pyhi = hh;
   }

   for (py = pylo + index; py < pyhi; py += NWT)
   {
      if (persp == 0) ffv = ffh = 1.0;
      else {
         ffv = 2.0 * persp * (py - pylo) / (pyhi - pylo);                        //  0 ... 2.0
         ffv = 1.0 + ffv * ffv;                                                  //  1 ... 5.0
         ffh = sqrtf(ffv);                                                       //  1 ... 2.23
      }

      dx = ampv * sin(2.0 * PI * py / (ffv * wlv));

      if (varv > 0)
         dx += ampv * varv * sin(2.0 * PI * py / (1.37 * ffv * wlv));            //  make irregular wave

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

         dy = amph * sin(2.0 * PI * px / (ffh * wlh));

         if (varh > 0)
            dy += amph * varh * sin(2.0 * PI * px / (1.37 * ffh * wlh));         //  make irregular wave

         vpixel(E1pxm,px+dx,py+dy,vpix);                                         //  source pixel, displaced
         red = vpix[0];
         green = vpix[1];
         blue = vpix[2];

         if (sa_stat == 3 && edist < sa_blend) {                                 //  blend edges
            f1 = sa_blendfunc(edist);                                            //  16.08
            f2 = 1.0 - f1;
            pix1 = PXMpix(E1pxm,px,py);
            red = f1 * red + f2 * pix1[0];
            green = f1 * green + f2 * pix1[1];
            blue = f1 * blue + f2 * pix1[2];
         }

         pix3 = PXMpix(E3pxm,px,py);                                             //  destination pixel
         pix3[0] = red;
         pix3[1] = green;
         pix3[2] = blue;
      }
   }

   exit_wthread();                                                               //  exit thread
   return 0;                                                                     //  not executed, stop gcc warning
}


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

//  Blur the image in one direction determined by dragging the mouse.
//  The effect diminishes to zero at the specified distance from the mouse.

namespace dirblur_names
{
   int         E3ww, E3hh;
   float       $mdx, $mdy, $mdw, $mdh;                                           //  blurfunc() and wthread()
   float       $D, $span, $intens;                                               //    use these $ args

   editfunc    EFdirblur;

   int    dirblur_dialog_event(zdialog *zd, cchar *event);
   void   dirblur_blurfunc();
   void   dirblur_mousefunc(void);
   void * dirblur_wthread(void *arg);
}


void m_dirblur(GtkWidget *, cchar *)                                             //  15.04
{
   using namespace dirblur_names;

   cchar  *dirblur_tip = ZTX("Pull image using the mouse.");

   F1_help_topic = "directed_blur";

   EFdirblur.menufunc = m_dirblur;
   EFdirblur.funcname = "dirblur";
   EFdirblur.mousefunc = dirblur_mousefunc;                                      //  mouse function
   if (! edit_setup(EFdirblur)) return;                                          //  setup edit

/***
          ____________________________________
         | Pull image using the mouse.        |
         |                                    |
         | blur span  [ 0.23  -+]             |
         | intensity  [ 0.976 -+]             |
         |                                    |
         |           [Reset] [Done] [Cancel]  |
         |____________________________________|

***/

   zdialog *zd = zdialog_new(ZTX("Directed Blur"),Mwin,Breset,Bdone,Bcancel,null);
   EFdirblur.zd = zd;
   zdialog_add_widget(zd,"label","labm","dialog",dirblur_tip,"space=3");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","lab1","hb1",ZTX("blur span"),"space=8");
   zdialog_add_widget(zd,"spin","span","hb1","0.00|1.0|0.01|0.1","space=3");
   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"label","lab2","hb2",ZTX("intensity"),"space=8");
   zdialog_add_widget(zd,"spin","intens","hb2","0.00|1.0|0.01|0.1","space=3");

   E3ww = E3pxm->ww;                                                             //  preview dimensions
   E3hh = E3pxm->hh;

   zdialog_restore_inputs(zd);                                                   //  restore previous inputs
   zdialog_fetch(zd,"span",$span);
   zdialog_fetch(zd,"intens",$intens);

   zdialog_run(zd,dirblur_dialog_event,"save");                                  //  run dialog, parallel
   takeMouse(dirblur_mousefunc,dragcursor);                                      //  connect mouse function
   return;
}


//  dialog event and completion callback function

int dirblur_names::dirblur_dialog_event(zdialog * zd, cchar *event)
{
   using namespace dirblur_names;

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

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

   if (strmatch(event,"span"))
      zdialog_fetch(zd,"span",$span);

   if (strmatch(event,"intens"))
      zdialog_fetch(zd,"intens",$intens);

   return 1;
}


//  dirblur mouse function

void dirblur_names::dirblur_mousefunc(void)
{
   using namespace dirblur_names;

   if (Mxdrag || Mydrag)                                                         //  mouse drag underway
   {
      $mdx = Mxdown;                                                             //  drag origin
      $mdy = Mydown;
      $mdw = Mxdrag - Mxdown;                                                    //  drag increment
      $mdh = Mydrag - Mydown;
      dirblur_blurfunc();                                                        //  drag image
      Mxdrag = Mydrag = 0;
      return;
   }

   return;
}


//  blur image and accumulate blur memory
//  mouse at (mx,my) is moved (mw,mh) pixels

void dirblur_names::dirblur_blurfunc()
{
   using namespace dirblur_names;

   float       D, d1, d2, d3, d4;

   d1 = ($mdx-0) * ($mdx-0) + ($mdy-0) * ($mdy-0);                               //  distance, mouse to 4 corners
   d2 = (E3pxm->ww-$mdx) * (E3pxm->ww-$mdx) + ($mdy-0) * ($mdy-0);
   d3 = (E3pxm->ww-$mdx) * (E3pxm->ww-$mdx) + (E3pxm->hh-$mdy) * (E3pxm->hh-$mdy);
   d4 = ($mdx-0) * ($mdx-0) + (E3pxm->hh-$mdy) * (E3pxm->hh-$mdy);

   D = d1;
   if (d2 > D) D = d2;                                                           //  find greatest corner distance
   if (d3 > D) D = d3;
   if (d4 > D) D = d4;

   $D = D * $span;

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

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


//  working thread to process the pixels

void * dirblur_names::dirblur_wthread(void *arg)
{
   using namespace dirblur_names;

   int      index = *((int *) arg);
   int      px, py, vstat;
   float    d, mag, dispx, dispy;
   float    F1, F2;
   float    vpix[4], *pix3;

   F1 = $intens * $intens;
   F2 = 1.0 - F1;

   for (py = index; py < E3pxm->hh; py += NWT)                                   //  process all pixels
   for (px = 0; px < E3pxm->ww; px++)
   {
      d = (px-$mdx)*(px-$mdx) + (py-$mdy)*(py-$mdy);
      mag = (1.0 - d / $D);
      if (mag < 0) continue;

      mag = mag * mag;                                                           //  faster than pow(mag,4);
      mag = mag * mag;

      dispx = -$mdw * mag;                                                       //  displacement = drag * mag
      dispy = -$mdh * mag;

      vstat = vpixel(E3pxm,px+dispx,py+dispy,vpix);                              //  input virtual pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      if (vstat) {
         pix3[0] = F2 * pix3[0] + F1 * vpix[0];                                  //  output = input pixel blend
         pix3[1] = F2 * pix3[1] + F1 * vpix[1];
         pix3[2] = F2 * pix3[2] + F1 * vpix[2];
      }
   }

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


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

//  Blur Background
//  Blur the image outside of a selected area or areas. 
//  Blur increases with distance from selected area edges.

namespace blur_BG_names 
{
   int         conrad, incrad;               //  constant or increasing blur
   int         conbrad;                      //  constant blur radius
   int         minbrad;                      //  min. blur radius
   int         maxbrad;                      //  max. blur radius
   VOL int     cancel;                       //  GCC inconsistent
   int         ww, hh;                       //  image dimensions
   int         maxdist;                      //  max. area edge distance
   editfunc    EFblurBG;
}


//  menu function

void m_blur_BG(GtkWidget *, const char *)                                        //  new 16.03
{
   using namespace blur_BG_names;

   int    blur_BG_dialog_event(zdialog* zd, const char *event);
   void * blur_BG_thread(void *);

   F1_help_topic = "blur_background";

   EFblurBG.menufunc = m_blur_BG;
   EFblurBG.funcname = "blur_background";                                        //  function name
   EFblurBG.Farea = 2;                                                           //  select area usable (required)
   EFblurBG.threadfunc = blur_BG_thread;                                         //  thread function
      
   if (! edit_setup(EFblurBG)) return;                                           //  setup edit

   minbrad = 10;                                                                 //  defaults
   maxbrad = 20;
   conbrad = 10;
   conrad = 1;
   incrad = 0;
   cancel = 0;

   ww = E3pxm->ww;
   hh = E3pxm->hh;
   
/***
       ____________________________________
      |          Blur Background           |
      |                                    |
      |  [x] constant blur [ 20 |-+]       |
      |                                    |
      |  [x] increase blur with distance   |
      |    min. blur radius [ 20 |-+]      |
      |    max. blur radius [ 90 |-+]      |
      |                                    |
      |            [Apply] [Done] [Cancel] |
      |____________________________________|

***/

   zdialog *zd = zdialog_new(ZTX("Blur Background"),Mwin,Bapply,Bdone,Bcancel,null);
   CEF->zd = zd;
   
   zdialog_add_widget(zd,"hbox","hbcon","dialog",0,"space=5");
   zdialog_add_widget(zd,"check","conrad","hbcon",ZTX("constant blur"),"space=3");
   zdialog_add_widget(zd,"spin","conbrad","hbcon","1|100|1|10","space=8");
   zdialog_add_widget(zd,"hbox","hbinc","dialog");
   zdialog_add_widget(zd,"check","incrad","hbinc",ZTX("increase blur with distance"),"space=3");
   zdialog_add_widget(zd,"hbox","hbmin","dialog");
   zdialog_add_widget(zd,"label","labmin","hbmin",ZTX("min. blur radius"),"space=8");
   zdialog_add_widget(zd,"spin","minbrad","hbmin","0|100|1|10","space=3");
   zdialog_add_widget(zd,"hbox","hbmax","dialog");
   zdialog_add_widget(zd,"label","labmax","hbmax",ZTX("max. blur radius"),"space=8");
   zdialog_add_widget(zd,"spin","maxbrad","hbmax","1|100|1|20","space=3");
   
   zdialog_stuff(zd,"conrad",conrad);
   zdialog_stuff(zd,"incrad",incrad);

   zdialog_resize(zd,300,0);
   zdialog_restore_inputs(zd);                                                   //  restore previous inputs
   zdialog_run(zd,blur_BG_dialog_event,"save");                                  //  run dialog - parallel
   
   if (sa_stat != 3) m_select(0,0);                                              //  start select area dialog

   F1_help_topic = "blur_background";                                            //  don't lose the help topic

   return;
}


//  blur_BG dialog event and completion function

int blur_BG_dialog_event(zdialog *zd, const char *event)                         //  blur_BG dialog event function
{
   using namespace blur_BG_names;
   
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"enter")) zd->zstat = 2;                                   //  KB input
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   if (strmatch(event,"escape")) zd->zstat = 3;                                  //  escape = cancel

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

         if (sa_stat != 3) {
            zmessageACK(Mwin,ZTX("no active Select Area"));
            m_select(0,0);
            return 1;
         }
         
         if (incrad && ! sa_calced)                                              //  if increading blur radius,
            sa_edgecalc();                                                       //    calc. area edge distances

         sa_show(0);
         edit_reset();
         signal_thread();
         return 1;
      }
      
      else if (zd->zstat == 2) {
         edit_done(0);                                                           //  [done]
         if (zdsela) zdialog_send_event(zdsela,"done");                          //  kill select area dialog
      }

      else {
         cancel = 1;                                                             //  kill threads
         edit_cancel(0);                                                         //  [cancel] or [x], discard edit
         if (zdsela) zdialog_send_event(zdsela,"done");
      }

      return 1;
   }
   
   if (strstr("conrad incrad",event)) {                                          //  16.07
      zdialog_stuff(zd,"conrad",0);
      zdialog_stuff(zd,"incrad",0);
      zdialog_stuff(zd,event,1);
   }
   
   zdialog_fetch(zd,"conrad",conrad);
   zdialog_fetch(zd,"incrad",incrad);
   zdialog_fetch(zd,"conbrad",conbrad);
   zdialog_fetch(zd,"minbrad",minbrad);
   zdialog_fetch(zd,"maxbrad",maxbrad);
   
   return 1;
}


//  thread function - multiple working threads to update image

void * blur_BG_thread(void *)
{
   using namespace blur_BG_names;

   void  * blur_BG_wthread(void *arg);                                           //  worker thread
   
   int      ii, dist;

   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request
      
      if (incrad && sa_calced) {                                                 //  if increasing blur radius,
         maxdist = 0;                                                            //    get max. area edge distance
         for (ii = 0; ii < ww * hh; ii++) {
            dist = sa_pixmap[ii];
            if (dist > maxdist) maxdist = dist;
         }
      }

      Fbusy_goal = sa_Npixel;
      Fbusy_done = 0;

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

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

      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop warning
}


void * blur_BG_wthread(void *arg)                                                //  worker thread function
{
   using namespace blur_BG_names;

   int         index = *((int *) (arg));
   int         ii, rad = 0, dist, npix;
   int         px, py, qx, qy;
   float       *pix1, *pix3;
   float       red, green, blue, F;
   
   for (py = index; py < hh; py += NWT)                                          //  loop all image pixels
   {
      if (cancel) break;                                                         //  cancel edit
      
      for (px = 0; px < ww; px++)
      {
         ii = py * ww + px;
         dist = sa_pixmap[ii];                                                   //  area edge distance
         if (! dist) continue;                                                   //  pixel outside the area
         
         if (conrad) rad = conbrad;                                              //  use constant blur radius

         if (incrad) {                                                           //  use increasing blur radius
            if (! sa_calced) exit_wthread();                                     //    depending on edge distance
            rad = minbrad + (maxbrad - minbrad) * dist / maxdist;
         }

         npix = 0;
         red = green = blue = 0;      

         for (qy = py-rad; qy <= py+rad; qy++)                                   //  average surrounding pixels
         for (qx = px-rad; qx <= px+rad; qx++)
         {
            if (qy < 0 || qy > hh-1) continue;
            if (qx < 0 || qx > ww-1) continue;
            ii = qy * ww + qx;
            if (! sa_pixmap[ii]) continue;
            pix1 = PXMpix(E1pxm,qx,qy);
            red += pix1[0];
            green += pix1[1];
            blue += pix1[2];
            npix++;
         }
         
         F = 0.999 / npix;
         red = F * red;                                                          //  blurred pixel RGB
         green = F * green;
         blue = F * blue;
         pix3 = PXMpix(E3pxm,px,py);
         pix3[0] = red;
         pix3[1] = green;
         pix3[2] = blue;

         Fbusy_done++;                                                           //  count pixels done
      }
   }
   
   exit_wthread();                                                               //  exit thread
   return 0;                                                                     //  not executed, avoid gcc warning
}



