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

   Fotoxx      edit photos and manage collections

   Copyright 2007 2008 2009 2010 2011 2012 Michael Cornelison
   Source URL: http://kornelix.squarespace.com/fotoxx
   Contact: kornelix2@googlemail.com
   
   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

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

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

#define EX extern                                                          //  enable extern declarations
#include "fotoxx.h"


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

   Fotoxx image edit - arty transformation functions

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


//  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");
   
   zfuncs::F1_help_topic = "color_depth";                                  //  v.10.8

   EFcolordep.funcname = "color-depth";
   EFcolordep.Fprev = 1;                                                   //  use preview
   EFcolordep.Farea = 2;                                                   //  select area usable
   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,"label","lab1","dialog",colmess,"space=5");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10");
   zdialog_add_widget(zd,"spin","colors","hb1","1|16|1|16","space=5");

   zdialog_help(zd,"color_depth");                                         //  zdialog help topic        v.11.08
   zdialog_run(zd,colordep_dialog_event,"-10/20");                         //  run dialog, parallel            v.11.07
   
   colordep_depth = 16;
   return;
}


//  dialog event and completion callback function

int colordep_dialog_event(zdialog *zd, cchar *event)
{
   if (zd->zstat)                                                          //  dialog complete
   {
      if (zd->zstat == 1) edit_done(EFcolordep);
      else edit_cancel(EFcolordep);
      return 0;
   }

   if (strEqu(event,"undo")) edit_undo();                                  //  v.10.2
   if (strEqu(event,"redo")) edit_redo();                                  //  v.10.3
   if (strEqu(event,"blendwidth")) signal_thread();                        //  v.10.3

   if (strEqu(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;
   uint16      *pix1, *pix3;
   double      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 < E3hh; py++)
      for (px = 0; px < E3ww; px++)
      {
         if (Factivearea) {                                                //  select area active
            ii = py * Fww + px;
            dist = sa_pixmap[ii];                                          //  distance from edge
            if (! dist) continue;                                          //  outside pixel
         }

         pix1 = PXMpix(E1pxm16,px,py);                                     //  input pixel
         pix3 = PXMpix(E3pxm16,px,py);                                     //  output pixel
         
         for (rgb = 0; rgb < 3; rgb++)
         {
            val1 = pix1[rgb];
            if (val1 < m1) val3 = (val1 + m2) & m1;
            else val3 = m1;
            val3 = uint(val3 * fmag);

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

            pix3[rgb] = val3;
         }
      }

      CEF->Fmod = 1;
      mwpaint2();                                                          //  update window
   }

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


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

//  convert image to simulate a drawing

int      draw_contrast;
int      draw_threshold;
int      draw_pixcon;
int      draw_reverse;
double   draw_trfunc[256];
double   draw_pixcon3;
uint8    *draw_pixcon_map = 0;

editfunc    EFdraw;


void m_draw(GtkWidget *, cchar *)
{
   int    draw_dialog_event(zdialog* zd, cchar *event);
   void * draw_thread(void *);

   cchar       *title = ZTX("Simulate Drawing");
   uint16      *pix1, *pix2;
   int         ii, px, py, qx, qy;
   int         red, green, blue, con, maxcon;

   zfuncs::F1_help_topic = "drawing";                                      //  v.10.8

   EFdraw.funcname = "drawing";
   EFdraw.Farea = 2;                                                       //  select area usable
   EFdraw.threadfunc = draw_thread;                                        //  thread function
   if (! edit_setup(EFdraw)) return;                                       //  setup edit

   draw_pixcon_map = (uint8 *) zmalloc(E1ww*E1hh,"draw");                  //  set up pixel contrast map
   memset(draw_pixcon_map,0,E1ww*E1hh);

   for (py = 1; py < E1hh-1; py++)                                         //  scan image pixels
   for (px = 1; px < E1ww-1; px++)
   {
      pix1 = PXMpix(E1pxm16,px,py);                                        //  pixel at (px,py)
      red = pix1[0];                                                       //  pixel RGB levels
      green = pix1[1];
      blue = pix1[2];
      maxcon = 0;

      for (qy = py-1; qy < py+2; qy++)                                     //  loop 3x3 block of neighbor
      for (qx = px-1; qx < px+2; qx++)                                     //    pixels around pix1
      {
         pix2 = PXMpix(E1pxm16,qx,qy);                                     //  find max. contrast with
         con = abs(red-pix2[0]) + abs(green-pix2[1]) + abs(blue-pix2[2]);  //    neighbor pixel
         if (con > maxcon) maxcon = con;
      }

      ii = py * E1ww + px;
      draw_pixcon_map[ii] = (maxcon/3) >> 8;                               //  contrast for (px,py) 0-255
   }

   zdialog *zd = zdialog_new(title,mWin,Bdone,Bcancel,null);
   EFdraw.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|expand");
   zdialog_add_widget(zd,"label","lab1","vb1",ZTX("contrast"));
   zdialog_add_widget(zd,"label","lab2","vb1",Bthresh);
   zdialog_add_widget(zd,"label","lab3","vb1",ZTX("outlines"));
   zdialog_add_widget(zd,"hscale","contrast","vb2","0|100|1|0","expand");
   zdialog_add_widget(zd,"hscale","threshold","vb2","0|100|1|0","expand");
   zdialog_add_widget(zd,"hscale","pixcon","vb2","0|255|1|0","expand");
   zdialog_add_widget(zd,"hbox","hb4","dialog");
   zdialog_add_widget(zd,"radio","pencil","hb4",ZTX("pencil"),"space=10");
   zdialog_add_widget(zd,"radio","chalk","hb4",ZTX("chalk"),"space=10");

   zdialog_resize(zd,300,0);
   zdialog_help(zd,"drawing");                                             //  zdialog help topic        v.11.08
   zdialog_run(zd,draw_dialog_event,"-10/20");                             //  run dialog, parallel            v.11.07

   return;
}


//  dialog event and completion callback function

int draw_dialog_event(zdialog *zd, cchar *event)                           //  draw dialog event function
{
   if (zd->zstat)                                                          //  dialog complete
   {
      if (zd->zstat == 1) edit_done(EFdraw);
      else edit_cancel(EFdraw);
      zfree(draw_pixcon_map);
      return 0;
   }

   if (strEqu(event,"undo")) edit_undo();                                  //  v.10.2
   if (strEqu(event,"redo")) edit_redo();                                  //  v.10.3
   if (strEqu(event,"blendwidth")) signal_thread();                        //  v.10.3

   if (strcmpv(event,"contrast","threshold","pixcon","chalk",null)) 
   {
      zdialog_fetch(zd,"contrast",draw_contrast);                          //  get slider values
      zdialog_fetch(zd,"threshold",draw_threshold);
      zdialog_fetch(zd,"pixcon",draw_pixcon);
      zdialog_fetch(zd,"chalk",draw_reverse);
      signal_thread();                                                     //  trigger update thread
   }

   return 1;
}


//  thread function - use multiple working threads

void * draw_thread(void *)
{
   void  * draw_wthread(void *arg);

   int         ii;
   double      threshold, contrast, trf;
   
   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request

      threshold = 0.01 * draw_threshold;                                   //  range 0 to 1
      contrast = 0.01 * draw_contrast;                                     //  range 0 to 1
      
      for (ii = 0; ii < 256; ii++)                                         //  brightness transfer function
      {
         trf = 1.0 - 0.003906 * (256 - ii) * contrast;                     //  ramp-up from 0-1 to 1
         if (ii < 256 * threshold) trf = 0;                                //  0 if below threshold
         draw_trfunc[ii] = trf;
      }
      
      for (ii = 0; ii < Nwt; ii++)                                         //  start worker threads
         start_wthread(draw_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion

      CEF->Fmod = 1;
      mwpaint2();                                                          //  update window
   }

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


void * draw_wthread(void *arg)                                             //  worker thread function
{
   void  draw_1pix(int px, int py);

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

   pixcon = draw_pixcon / 255.0;                                           //  0-1 linear ramp
   draw_pixcon3 = 255 * pixcon * pixcon * pixcon;                          //  0-255 cubic ramp

   for (py = index+1; py < E1hh-1; py += Nwt)                              //  process all pixels
   for (px = 1; px < E1ww-1; px++)
      draw_1pix(px,py);

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


void draw_1pix(int px, int py)                                             //  process one pixel
{
   uint16      *pix1, *pix3;
   int         ii, dist = 0;
   int         bright1, bright2;
   int         red1, green1, blue1;
   int         red3, green3, blue3;
   double      dold, dnew;
   double      pixcon = draw_pixcon3;

   if (Factivearea) {                                                      //  select area active
      ii = py * Fww + px;
      dist = sa_pixmap[ii];                                                //  distance from edge
      if (! dist) return;                                                  //  outside pixel
   }
   
   pix1 = PXMpix(E1pxm16,px,py);                                           //  input pixel
   pix3 = PXMpix(E3pxm16,px,py);                                           //  output pixel
      
   red1 = pix1[0];
   green1 = pix1[1];
   blue1 = pix1[2];
   
   bright1 = ((red1 + green1 + blue1) / 3) >> 8;                           //  old brightness  0-255
   bright2 = bright1 * draw_trfunc[bright1];                               //  new brightness  0-255
   
   ii = py * E1ww + px;
   if (draw_pixcon_map[ii] < pixcon) bright2 = 255;

   if (pixcon > 1 && bright2 > draw_threshold) bright2 = 255;              //  empirical !!!

   if (draw_reverse) bright2 = 255 - bright2;                              //  negate if "chalk"

   red3 = green3 = blue3 = bright2 << 8;                                   //  gray scale, new brightness

   if (Factivearea && dist < sa_blend) {                                   //  select area is active,
      dnew = 1.0 * dist / sa_blend;
      dold = 1.0 - dnew;
      red3 = dnew * red3 + dold * red1;
      green3 = dnew * green3 + dold * green1;
      blue3 = dnew * blue3 + dold * blue1;
   }

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


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

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

double   outlines_olth, outlines_olww, outlines_imbr;

editfunc    EFoutlines;


void m_outlines(GtkWidget *, cchar *)
{
   int    outlines_dialog_event(zdialog* zd, cchar *event);
   void * outlines_thread(void *);

   cchar  *title = ZTX("Add Image Outlines");

   zfuncs::F1_help_topic = "outlines";
   
   EFoutlines.funcname = "outlines";
   EFoutlines.Farea = 2;                                                   //  select area usable
   EFoutlines.threadfunc = outlines_thread;                                //  thread function
   if (! edit_setup(EFoutlines)) return;                                   //  setup edit

   zdialog *zd = zdialog_new(title,mWin,Bdone,Bcancel,null);
   EFoutlines.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","lab1","hb1",ZTX("outline threshold"),"space=5");
   zdialog_add_widget(zd,"hscale","olth","hb1","0|100|1|90","expand|space=5");   
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","lab2","hb2",ZTX("outline width"),"space=5");
   zdialog_add_widget(zd,"hscale","olww","hb2","0|100|1|50","expand|space=5");
   zdialog_add_widget(zd,"hbox","hb3","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","lab3","hb3",ZTX("image brightness"),"space=5");
   zdialog_add_widget(zd,"hscale","imbr","hb3","0|100|1|10","expand|space=5");
   
   outlines_olth = 90;
   outlines_olww = 50;
   outlines_imbr = 10;

   zdialog_resize(zd,300,0);
   zdialog_help(zd,"outlines");                                            //  zdialog help topic        v.11.08
   zdialog_run(zd,outlines_dialog_event,"-10/20");                         //  run dialog, parallel            v.11.07

   signal_thread();
   return;
}


//  dialog event and completion callback function

int outlines_dialog_event(zdialog *zd, cchar *event)                       //  dialog event function
{
   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(EFoutlines);                           //  done
      else edit_cancel(EFoutlines);                                        //  cancel or destroy
      PXM_free(E8pxm16);
      PXM_free(E9pxm16);
      return 0;
   }

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

   if (strEqu(event,"olww")) {
      zdialog_fetch(zd,"olww",outlines_olww);                              //  get outline width 0-100
      signal_thread();
   }
   
   if (strEqu(event,"imbr")) {
      zdialog_fetch(zd,"imbr",outlines_imbr);                              //  get image brightness 0-100
      signal_thread();
   }
   
   if (strEqu(event,"undo")) edit_undo();
   if (strEqu(event,"redo")) edit_redo();
   if (strEqu(event,"blendwidth")) signal_thread();

   return 1;
}


//  thread function to update image

void * outlines_thread(void *)
{
   void * outlines_wthread(void *arg);
   
   int         px, py, ww, hh;
   double      olww, red3, green3, blue3;
   double      red3h, red3v, green3h, green3v, blue3h, blue3v;
   uint16      *pix8, *pix9;
   uint16      *pixa, *pixb, *pixc, *pixd, *pixe, *pixf, *pixg, *pixh;

   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request
      
      olww = 0.01 * outlines_olww;                                         //  outline width, 0 - 1.0
      olww = 1.0 - olww;                                                   //  1.0 - 0
      olww = 0.8 * olww + 0.2;                                             //  1.0 - 0.2

      ww = Fww * olww + 0.5;                                               //  create smaller outline image
      hh = Fhh * olww + 0.5;
      
      if (! E9pxm16 || ww != E9pxm16->ww)                                  //  initial or changed outline brightness
      {
         PXM_free(E8pxm16);
         PXM_free(E9pxm16);

         E8pxm16 = PXM_rescale(E1pxm16,ww,hh);
         E9pxm16 = PXM_copy(E8pxm16);

         for (py = 1; py < hh-1; py++)
         for (px = 1; px < ww-1; px++)
         {
            pix8 = PXMpix(E8pxm16,px,py);                                  //  input pixel
            pix9 = PXMpix(E9pxm16,px,py);                                  //  output pixel
            
            pixa = pix8-3*ww-3;                                            //  8 neighboring pixels are used
            pixb = pix8-3*ww;                                              //    to get edge brightness using
            pixc = pix8-3*ww+3;                                            //       a Sobel filter
            pixd = pix8-3;
            pixe = pix8+3;
            pixf = pix8+3*ww-3;
            pixg = pix8+3*ww;
            pixh = pix8+3*ww+3;
            
            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 = (abs(red3h) + abs(red3v)) / 2;                          //  average vertical and horizontal brightness
            green3 = (abs(green3h) + abs(green3v)) / 2;
            blue3 = (abs(blue3h) + abs(blue3v)) / 2;
            
            if (red3 > 65535) red3 = 65535;
            if (green3 > 65535) green3 = 65535;
            if (blue3 > 65535) blue3 = 65535;

            pix9[0] = red3;
            pix9[1] = green3;
            pix9[2] = blue3;
         }
      }
      
      PXM_free(E8pxm16);                                                   //  scale back to full-size
      E8pxm16 = PXM_rescale(E9pxm16,Fww,Fhh);

      for (int ii = 0; ii < Nwt; ii++)                                     //  start worker threads
         start_wthread(outlines_wthread,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for completion

      CEF->Fmod = 1;
      mwpaint2();                                                          //  update window
   }

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


void * outlines_wthread(void *arg)                                         //  worker thread function
{
   int         index = *((int *) arg);
   int         px, py, ii, dist = 0;
   double      olth, imbr, br8, dold, dnew;
   double      red1, green1, blue1, red3, green3, blue3;
   uint16      *pix1, *pix8, *pix3;
   
   olth = 0.01 * outlines_olth;                                            //  outline threshold, 0 - 1.0
   olth = 1.0 - olth;                                                      //                     1.0 - 0
   imbr = 0.01 * outlines_imbr;                                            //  image brightness,  0 - 1.0

   for (py = index+1; py < Fhh-1; py += Nwt)
   for (px = 1; px < Fww-1; px++)
   {
      if (Factivearea) {                                                   //  select area active
         ii = py * Fww + px;
         dist = sa_pixmap[ii];                                             //  distance from edge
         if (! dist) continue;                                             //  pixel outside area
      }
      
      pix1 = PXMpix(E1pxm16,px,py);                                        //  input image pixel
      pix8 = PXMpix(E8pxm16,px,py);                                        //  input outline pixel
      pix3 = PXMpix(E3pxm16,px,py);                                        //  output image pixel
      
      red1 = pix1[0];                                                      //  input image pixel
      green1 = pix1[1];
      blue1 = pix1[2];
      
      br8 = pixbright(pix8);                                               //  outline brightness
      br8 = br8 / 65536.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 (Factivearea && dist < sa_blend) {                                //  select area is active,
         dnew = 1.0 * dist / sa_blend;                                     //    blend changes over sa_blend
         dold = 1.0 - dnew;
         red3 = dnew * red3 + dold * red1;
         green3 = dnew * green3 + dold * green1;
         blue3 = dnew * blue3 + dold * blue1;
      }
      
      pix3[0] = red3;
      pix3[1] = green3;
      pix3[2] = blue3;
   }
   
   exit_wthread();
   return 0;                                                               //  not executed, stop gcc warning
}


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

//  convert image to simulate an embossing

int      emboss_radius, emboss_color;
double   emboss_depth;
double   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");

   zfuncs::F1_help_topic = "embossing";                                    //  v.10.8

   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=10");
   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",ZTX("color"),"space=8");

   zdialog_help(zd,"embossing");                                           //  zdialog help topic        v.11.08
   zdialog_run(zd,emboss_dialog_event,"-10/20");                           //  run dialog, parallel            v.11.07
   return;
}


//  dialog event and completion callback function

int emboss_dialog_event(zdialog *zd, cchar *event)                         //  emboss dialog event function
{
   if (zd->zstat)                                                          //  dialog complete
   {
      if (zd->zstat == 1) edit_done(EFemboss);
      else edit_cancel(EFemboss);
      return 0;
   }

   if (strEqu(event,"undo")) edit_undo();                                  //  v.10.2
   if (strEqu(event,"redo")) edit_redo();                                  //  v.10.3
   if (strEqu(event,"blendwidth")) signal_thread();                        //  v.10.3

   if (strcmpv(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;
   double      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,&wtnx[ii]);
      wait_wthreads();                                                     //  wait for comletion

      CEF->Fmod = 1;
      mwpaint2();                                                          //  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 < E1hh; py += Nwt)                                  //  process all pixels
   for (px = 0; px < E1ww; 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;
   int         bright1, bright3;
   int         rgb, dx, dy, rad;
   uint16      *pix1, *pix3, *pixN;
   double      sumpix, kern, dold, dnew;
   
   if (Factivearea) {                                                      //  select area active
      ii = py * Fww + px;
      dist = sa_pixmap[ii];                                                //  distance from edge
      if (! dist) return;                                                  //  outside pixel
   }

   rad = emboss_radius;

   if (px < rad || py < rad) return;
   if (px > E3ww-rad-1 || py > E3hh-rad-1) return;
   
   pix1 = PXMpix(E1pxm16,px,py);                                           //  input pixel
   pix3 = PXMpix(E3pxm16,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 * E1ww + dx) * 3;
            kern = emboss_kernel[dx+rad][dy+rad];
            sumpix += kern * pixN[rgb];
      
            bright1 = pix1[rgb];
            bright3 = sumpix;
            if (bright3 < 0) bright3 = 0;
            if (bright3 > 65535) bright3 = 65535;

            if (Factivearea && dist < sa_blend) {                          //  select area is active,
               dnew = 1.0 * dist / sa_blend;                               //    blend changes over sa_blend
               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 * E1ww + dx) * 3;
         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 > 65535) bright3 = 65535;
      
      if (Factivearea && dist < sa_blend) {                                //  select area is active,
         dnew = 1.0 * dist / sa_blend;                                     //    blend changes over sa_blend
         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;
uint16      *tile_pixmap = 0;

editfunc    EFtiles;


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

   zfuncs::F1_help_topic = "tiles";                                        //  v.10.8

   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,Bdone,Bcancel,null);
   EFtiles.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labt","hb1",ZTX("tile size"),"space=5");
   zdialog_add_widget(zd,"spin","size","hb1","1|99|1|5","space=5");
   zdialog_add_widget(zd,"button","apply","hb1",Bapply,"space=10");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   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_help(zd,"tiles");                                               //  zdialog help topic        v.11.08
   zdialog_run(zd,tiles_dialog_event,"-10/20");                            //  run dialog, parallel            v.11.07

   tile_size = 5;
   tile_gap = 1;

   tile_pixmap = (uint16 *) zmalloc(E1ww*E1hh*6,"tiles");                  //  set up pixel color map
   memset(tile_pixmap,0,E1ww*E1hh*6);

   return;
}


//  dialog event and completion callback function

int tiles_dialog_event(zdialog * zd, cchar *event)
{
   if (zd->zstat)                                                          //  dialog complete
   {
      if (zd->zstat == 1) edit_done(EFtiles);
      else edit_cancel(EFtiles);
      zfree(tile_pixmap);
      return 0;
   }

   if (strEqu(event,"undo")) edit_undo();                                  //  v.10.2
   if (strEqu(event,"redo")) edit_redo();                                  //  v.10.3
   if (strEqu(event,"blendwidth")) signal_thread();                        //  v.10.3

   if (strNeq(event,"apply")) return 0;

   zdialog_fetch(zd,"size",tile_size);                                     //  get tile size
   zdialog_fetch(zd,"gap",tile_gap);                                       //  get tile gap 

   if (tile_size < 2) {
      edit_reset();                                                        //  restore original image
      return 0;
   }
   
   signal_thread();                                                        //  trigger working thread
   return 1;
}


//  image tiles thread function

void * tiles_thread(void *)
{
   int         sg, gg;
   int         sumpix, red, green, blue;
   int         ii, jj, px, py, qx, qy, dist;
   double      dnew, dold;
   uint16      *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 < E1hh; py += sg)                                    //  initz. pixel color map for
      for (px = 0; px < E1ww; 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 > E1hh-1 || qx > E1ww-1) continue;

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

         if (sumpix) {
            red = (red / sumpix);
            green = (green / sumpix);
            blue = (blue / sumpix);
         }
         
         for (qy = py; qy < py + sg; qy++)                                 //  set color for pixels in block
         for (qx = px; qx < px + sg; qx++)
         {
            if (qy > E1hh-1 || qx > E1ww-1) continue;
            
            jj = (qy * E1ww + qx) * 3;

            if (qx-px < gg || qy-py < gg) {
               tile_pixmap[jj] = tile_pixmap[jj+1] = tile_pixmap[jj+2] = 0;
               continue;
            }

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

      if (Factivearea)                                                     //  process selected area
      {
         for (ii = 0; ii < Fww * Fhh; ii++)                                //  find pixels in select area
         {                                                                 //  v.9.6
            if (! sa_pixmap[ii]) continue;
            py = ii / Fww;
            px = ii - py * Fww;
            dist = sa_pixmap[ii];                                          //  distance from edge
            pix3 = PXMpix(E3pxm16,px,py);
            jj = (py * E3ww + px) * 3;

            if (dist >= sa_blend) {                                        //  blend changes over sa_blend   v.10.2
               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 = 1.0 * dist / sa_blend;
               dold = 1.0 - dnew;
               pix1 = PXMpix(E1pxm16,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 < E3hh-1; py++)                                   //  loop all image pixels
         for (px = 0; px < E3ww-1; px++)
         {
            pix3 = PXMpix(E3pxm16,px,py);                                  //  target pixel
            jj = (py * E3ww + 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->Fmod = 1;
      mwpaint2();                                                          //  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 *)                                          //  v.11.01
{
   int    dots_dialog_event(zdialog *zd, cchar *event);
   void * dots_thread(void *);

   zfuncs::F1_help_topic = "dot_matrix";

   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=5");
   zdialog_add_widget(zd,"label","labt","hb1",ZTX("dot size"),"space=5");
   zdialog_add_widget(zd,"spin","size","hb1","3|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");

   zdialog_help(zd,"dot_matrix");                                          //  zdialog help topic        v.11.08
   zdialog_run(zd,dots_dialog_event,"-10/20");                             //  run dialog, parallel            v.11.07

   dot_size = 9;
   return;
}


//  dialog event and completion callback function

int dots_dialog_event(zdialog * zd, cchar *event)
{
   if (zd->zstat)                                                          //  dialog complete
   {
      if (zd->zstat == 1) edit_done(EFdots);
      else edit_cancel(EFdots);
      return 0;
   }

   if (strEqu(event,"undo")) edit_undo();
   if (strEqu(event,"redo")) edit_redo();
   if (strEqu(event,"blendwidth")) signal_thread();
   if (strNeq(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, sumpix, red, green, blue;
   int         cc, maxrgb, ii, dist, shift1, shift2;
   int         px, py, px1, px2, py1, py2;
   int         qx, qy, qx1, qx2, qy1, qy2;
   uint16      *pix1, *pix3;
   double      relmax, radius, radius2a, radius2b, f1, f2;
   double      f64K = 1.0 / 65536.0;
   double      fpi = 1.0 / 3.14159;
   double      dcx, dcy, qcx, qcy, qdist2;
   
   while (true)
   {
      thread_idle_loop();                                                  //  wait for work or exit request
      
      mutex_lock(&Fpixmap_lock); 

      ds = dot_size;                                                       //  dot size and tile size

      cc = E3ww * E3hh * 6;                                                //  clear output image to black
      memset(E3pxm16->bmp,0,cc);
      
      px1 = 0;                                                             //  limits for tiles 
      px2 = Fww - ds;
      py1 = 0;
      py2 = Fhh - ds;
      
      if (Factivearea) {                                                   //  reduce for active area
         px1 = sa_minx;
         px2 = sa_maxx;
         py1 = sa_miny;
         py2 = sa_maxy;
         if (px2 > Fww - ds) px2 = Fww - ds;
         if (py2 > Fhh - ds) py2 = Fhh - ds;
      }
      
      shift1 = 0;

      for (py = py1; py < py2; py += ds)                                   //  loop all tiles in input image
      {
         shift1 = 1 - shift1;                                              //  offset alternate rows   v.11.02
         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(E1pxm16,qx,qy);
               red += pix1[0];
               green += pix1[1];
               blue += pix1[2];
               sumpix++;
            }
            
            red = (red / sumpix);                                          //  mean RGB levels, 0 to 64K
            green = (green / sumpix);
            blue = (blue / sumpix);
            
            maxrgb = red;                                                  //  max. mean RGB level, 0 to 64K
            if (green > maxrgb) maxrgb = green;
            if (blue > maxrgb) maxrgb = blue;
            relmax = f64K * maxrgb;                                        //  max. RGB as 0 to 0.999
            
            radius = ds * sqrt(fpi * relmax);                              //  radius of dot with maximized color
            radius = radius * 0.9;                                         //  deliberate reduction      v.11.05
            if (radius < 0.5) continue;
            
            red = 65535.0 * red / maxrgb;                                  //  dot color, maximized
            green = 65535.0 * green / maxrgb;
            blue = 65535.0 * 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(E3pxm16,qx,qy);
               pix3[0] = f1 * red;
               pix3[1] = f1 * green;
               pix3[2] = f1 * blue;
            }
         }
      }
      
      if (Factivearea)                                                     //  select area active
      {
         pix1 = (uint16 *) E1pxm16->bmp;
         pix3 = (uint16 *) E3pxm16->bmp;
         
         for (ii = 0; ii < Fww * Fhh; ii++)                                //  loop all pixels
         {
            dist = sa_pixmap[ii];
            if (dist == 0) memmove(pix3,pix1,6);                           //  pixel outside area, output pixel = input
            else if (dist < sa_blend) {
               f1 = 1.0 * dist / sa_blend;                                 //  pixel in blend region
               f2 = 1.0 - f1;
               pix3[0] = f1 * pix3[0] + f2 * pix1[0];                      //  output pixel is mix of input and output
               pix3[1] = f1 * pix3[1] + f2 * pix1[1];
               pix3[2] = f1 * pix3[2] + f2 * pix1[2];
            }
            pix1 += 3;
            pix3 += 3;
         }
      }

      mutex_unlock(&Fpixmap_lock); 

      CEF->Fmod = 1;
      mwpaint2();                                                          //  update window
   }

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


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

//  convert image to simulate a painting
//  processing a 10 megapixel image needs 140 MB of main memory

namespace painting_names 
{
   editfunc    EFpainting;

   int         color_depth;
   int         group_area;
   double      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;
   uint16      gcolor[3];
}


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

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

   zfuncs::F1_help_topic = "painting";                                     //  v.10.8

   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_help(zd,"painting");                                            //  zdialog help topic        v.11.08
   zdialog_run(zd,painting_dialog_event,"-10/20");                         //  run dialog, parallel            v.11.07

   return;
}


//  dialog event and completion callback function

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

   if (zd->zstat)                                                          //  dialog complete
   {
      if (zd->zstat == 1) edit_done(EFpainting);
      else edit_cancel(EFpainting);
      return 0;
   }

   if (strEqu(event,"undo")) edit_undo();                                  //  v.10.2
   if (strEqu(event,"redo")) edit_redo();                                  //  v.10.3
   if (strEqu(event,"blendwidth")) signal_thread();                        //  v.10.3

   if (strEqu(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
      wait_thread_idle();
   }
   
   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->Fmod = 1;
      mwpaint2();                                                          //  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;
   double         fmag;
   uint16         m1, m2, val1, val3;
   uint16         *pix1, *pix3;

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

   fmag = 65535.0 / m1;                                                    //  full brightness range

   if (Factivearea)                                                        //  process select area
   {
      for (ii = 0; ii < Fww * Fhh; ii++)
      {                                                                    //  v.9.6
         if (! sa_pixmap[ii]) continue;
         py = ii / Fww;
         px = ii - py * Fww;

         pix1 = PXMpix(E1pxm16,px,py);                                     //  input pixel
         pix3 = PXMpix(E3pxm16,px,py);                                     //  output pixel

         for (rgb = 0; rgb < 3; rgb++)
         {
            val1 = pix1[rgb];
            if (val1 < m1) val3 = (val1 + m2) & m1;
            else val3 = m1;
            val3 = uint(val3 * fmag);
            pix3[rgb] = val3;
         }
      }
   }
   
   else                                                                    //  process entire image
   {
      for (py = 0; py < E3hh; py++)                                        //  loop all pixels
      for (px = 0; px < E3ww; px++)
      {
         pix1 = PXMpix(E1pxm16,px,py);                                     //  input pixel
         pix3 = PXMpix(E3pxm16,px,py);                                     //  output pixel
         
         for (rgb = 0; rgb < 3; rgb++)
         {
            val1 = pix1[rgb];
            if (val1 < m1) val3 = (val1 + m2) & m1;
            else val3 = m1;
            val3 = uint(val3 * fmag);
            pix3[rgb] = val3;
         }
      }
   }

   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;
   uint16         *pix3;

   cc1 = E3ww * E3hh;

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

   cc2 = cc1 * sizeof(spixstack);
   pixstack = (spixstack *) zmalloc(cc2,"painting");                       //  memory stack for pixel search
   memset(pixstack,0,cc2);
   
   cc2 = cc1 * sizeof(int);
   groupcount = (int *) zmalloc(cc2,"painting");                           //  counts pixels per group
   memset(groupcount,0,cc2);
   
   group = 0;
   
   for (py = 0; py < E3hh; py++)                                           //  loop all pixels
   for (px = 0; px < E3ww; px++)
   {
      kk = py * E3ww + px;
      if (Factivearea && ! sa_pixmap[kk]) continue;
      if (pixgroup[kk]) continue;                                          //  already assigned to group

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

      pix3 = PXMpix(E3pxm16,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          v.11.04
      pixstack[0].direc = 'a';
      Nstack = 1;

      while (Nstack)                                                       //  overhauled    v.11.02
      {
         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             v.11.04
            npx = px + px - ppx;
            npy = py + py - ppy;
            pixstack[kk].direc = 'r';                                      //  next search direction
         }

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

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

         if (npx < 0 || npx > E3ww-1) continue;                            //  pixel off the edge           v.11.04
         if (npy < 0 || npy > E3hh-1) continue;
         
         kk = npy * E3ww + npx;

         if (Factivearea && ! sa_pixmap[kk]) continue;                     //  pixel outside area

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

         pix3 = PXMpix(E3pxm16,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            v.11.04
      }
   }
   
   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;
   double      ff = 1.0 / 65536.0;
   double      fred, fgreen, fblue, match;
   int         nnpx[4] = {  0, -1, +1, 0 };
   int         nnpy[4] = { -1, 0,  0, +1 };
   uint16      *pix3, *pixN;

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

   snewgroup      *newgroup;
   
   nccc = (group + 1) * sizeof(snewgroup);
   newgroup = (snewgroup *) zmalloc(nccc,"painting");
   
   if (Factivearea)                                                        //  process select area
   {
      while (true)
      {
         memset(newgroup,0,nccc);

         for (ii = 0; ii < Fww * Fhh; ii++)                                //  find pixels in select area
         {                                                                 //  v.9.6
            if (! sa_pixmap[ii]) continue;
            py = ii / Fww;
            px = ii - py * Fww;

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

            pix3 = PXMpix(E3pxm16,px,py);

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

               if (npx < 0 || npx >= E3ww) continue;                       //  off the edge
               if (npy < 0 || npy >= E3hh) continue;

               kk = E3ww * npy + npx;
               if (! sa_pixmap[kk]) continue;                              //  pixel outside area
               if (pixgroup[kk] == group) continue;                        //  already in same group
               
               pixN = PXMpix(E3pxm16,npx,npy);                             //  match color of group neighbor
               fred = ff * abs(pix3[0] - pixN[0]);                         //    to color of group
               fgreen = ff * abs(pix3[1] - pixN[1]);
               fblue = ff * abs(pix3[2] - pixN[2]);
               match = (1.0 - fred) * (1.0 - fgreen) * (1.0 - fblue);      //  color match, 0 to 1.0
               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 < Fww * Fhh; ii++)                                //  find pixels in select area
         {                                                                 //  v.9.6
            if (! sa_pixmap[ii]) continue;
            py = ii / Fww;
            px = ii - py * Fww;

            kk = E3ww * 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(E3pxm16,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 < E3hh; py++)                                     //  loop all pixels
         for (px = 0; px < E3ww; px++)
         {
            kk = E3ww * py + px;                                           //  get assigned group
            group = pixgroup[kk];
            if (groupcount[group] >= group_area) continue;                 //  group count large enough

            pix3 = PXMpix(E3pxm16,px,py);

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

               if (npx < 0 || npx >= E3ww) continue;                       //  off the edge
               if (npy < 0 || npy >= E3hh) continue;
               
               kk = E3ww * npy + npx;
               if (pixgroup[kk] == group) continue;                        //  in same group

               pixN = PXMpix(E3pxm16,npx,npy);                             //  match color of group neighbor
               fred = ff * abs(pix3[0] - pixN[0]);                         //    to color of group
               fgreen = ff * abs(pix3[1] - pixN[1]);
               fblue = ff * abs(pix3[2] - pixN[2]);
               match = (1.0 - fred) * (1.0 - fgreen) * (1.0 - fblue);      //  color match, 0 to 1.0
               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 < E3hh; py++)                                     //  loop all pixels
         for (px = 0; px < E3ww; px++)
         {
            kk = E3ww * 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(E3pxm16,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;
   uint16         *pix3, *pixL, *pixA;
   
   if (! borders) return;
   
   cc = E3ww * E3hh;
   char * pixblack = zmalloc(cc,"painting");
   memset(pixblack,0,cc);

   if (Factivearea)
   {
      for (ii = 0; ii < Fww * Fhh; ii++)                                   //  find pixels in select area
      {                                                                    //  v.9.6
         if (! sa_pixmap[ii]) continue;
         py = ii / Fww;
         px = ii - py * Fww;
         if (px < 1 || py < 1) continue;

         pix3 = PXMpix(E3pxm16,px,py);
         pixL = PXMpix(E3pxm16,px-1,py);
         pixA = PXMpix(E3pxm16,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 - E3ww;
            if (pixblack[kk]) continue;
            kk += E3ww;
            pixblack[kk] = 1;
         }
      }

      for (ii = 0; ii < Fww * Fhh; ii++)                                   //  find pixels in select area
      {                                                                    //  v.9.6
         if (! sa_pixmap[ii]) continue;
         py = ii / Fww;
         px = ii - py * Fww;
         if (px < 1 || py < 1) continue;

         if (! pixblack[ii]) continue;
         pix3 = PXMpix(E3pxm16,px,py);
         pix3[0] = pix3[1] = pix3[2] = 0;
      }
   }
         
   else
   {
      for (py = 1; py < E3hh; py++)                                        //  loop all pixels
      for (px = 1; px < E3ww; px++)                                        //  omit top and left
      {
         pix3 = PXMpix(E3pxm16,px,py);                                     //  output pixel
         pixL = PXMpix(E3pxm16,px-1,py);                                   //  pixel to left
         pixA = PXMpix(E3pxm16,px,py-1);                                   //  pixel above
         
         if (pix3[0] != pixL[0] || pix3[1] != pixL[1] || pix3[2] != pixL[2])
         {
            kk = E3ww * 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 = E3ww * (py-1) + px;                                       //  have vertical transition
            if (pixblack[kk]) continue;
            kk += E3ww;
            pixblack[kk] = 1;
         }
      }

      for (py = 1; py < E3hh; py++)
      for (px = 1; px < E3ww; px++)
      {
         kk = E3ww * py + px;
         if (! pixblack[kk]) continue;
         pix3 = PXMpix(E3pxm16,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;
   uint16      *pix1, *pix3;
   double      f1, f2;
   
   if (Factivearea && sa_blend > 0)
   {
      for (ii = 0; ii < Fww * Fhh; ii++)                                   //  find pixels in select area
      {                                                                    //  v.9.6
         dist = sa_pixmap[ii];
         if (! dist || dist >= sa_blend) continue;

         py = ii / Fww;
         px = ii - py * Fww;
         pix1 = PXMpix(E1pxm16,px,py);                                     //  input pixel
         pix3 = PXMpix(E3pxm16,px,py);                                     //  output pixel

         f2 = 1.0 * dist / sa_blend;                                       //  changes over distance sa_blend
         f1 = 1.0 - f2;

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

   return;
}



