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

   Fotocx - edit photos and manage collections

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

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

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

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

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

   m_outlines              convert image to outline drawing
   m_sketch                convert photo to simulated sketch
   m_cartoon               convert image to cartoon drawing
   m_emboss                convert image to simulated embossing (3D relief)
   m_tiles1                convert image to square tiles with 3D edges
   m_tiles2                convert image to irregular tiles matching image
   m_painting              convert image to simulated painting
   m_texture               apply a random texture to an image
   m_pattern               tile an image with a repeating pattern
   m_dither                image dither functions:
   m_dither0                  Roy Lichtenstein effect
   m_dither1                  pure RGB or black/white dots
   m_dither2                  RGB mix with given bit-depth
   m_dither3                  use custom palette colors
   m_dither4                  pixels condense into blobs
   m_engrave               engrave image (black-white lines)
   m_mosaic                convert image to mosaic of thumbnail images
   m_add_noise             add random noise to an image
   m_set_colors            choose no. of colors for R/G/B or total
   m_shift_colors          gradually shift selected RGB colors into other colors
   m_alien_colors          change color hues with an algorithm
   m_anykernel             transform an image using any custom kernel

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

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

using namespace zfuncs;


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

//  make an outline drawing by maximizing high-contrast pixels and darkening the rest

namespace outlines_names
{
   editfunc    EFoutlines;
   float       Blo, Bhi;                                                         //  pixel contrast range to draw
   float       Amp;                                                              //  low contrast amplification
   int         ModeA, ModeB;                                                     //  operation mode
   int         Fbw, Fneg;                                                        //  flags
   float       *conmap;                                                          //  pixel contrast map
   int         Fconmap_done;
}

void m_outlines(GtkWidget *, ch *menu)                                           //  24.20
{
   using namespace outlines_names;

   int    outlines_dialog_event(zdialog* zd, ch *event);
   void * outlines_thread(void *);

   F1_help_topic = "outlines";

   Plog(1,"m_outlines \n");

   EFoutlines.menuname = "Outlines";
   EFoutlines.menufunc = m_outlines;
   EFoutlines.Farea = 2;                                                         //  select area usable
   EFoutlines.Fpaintedits = 1;                                                   //  can use paint edits
   EFoutlines.Fscript = 1;                                                       //  scripting supported 
   EFoutlines.threadfunc = outlines_thread;                                      //  thread function

   if (! edit_setup(EFoutlines)) return;                                         //  setup edit
 
/***
       ___________________________________
      |        Outline Drawing            |
      |                                   |
      | Threshold: Low [__]  High [__]    |                                      //  24.30
      | Low Amplify: [__]                 |                                      //  24.40
      | Mode: A (o)  B (o)                |                                      //  24.40
      | [_] black/white  [_] negative     |
      |                                   |
      |                          [OK] [X] |
      |___________________________________|

***/

   zdialog *zd = zdialog_new("Outline Drawing",Mwin,"OK"," X ",null);
   EFoutlines.zd = zd;

   zdialog_add_widget(zd,"hbox","hbT","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labLT","hbT","Threshold: Low","space=3");
   zdialog_add_widget(zd,"zspin","Blo","hbT","0|100|1|30","space=3");
   zdialog_add_widget(zd,"label","space","hbT",0,"space=10");
   zdialog_add_widget(zd,"label","labHT","hbT","High","space=3");
   zdialog_add_widget(zd,"zspin","Bhi","hbT","0|100|1|40","space=3");
   
   zdialog_add_widget(zd,"hbox","hbA","dialog",0,"space=3");                     //  24.40
   zdialog_add_widget(zd,"label","labamp","hbA","Low Amplify","space=3");
   zdialog_add_widget(zd,"zspin","Amp","hbA","0|10|1|0","space=3");
   
   zdialog_add_widget(zd,"hbox","hbM","dialog",0,"space=3");                     //  24.40
   zdialog_add_widget(zd,"label","labM","hbM","Mode:","space=3");  
   zdialog_add_widget(zd,"radio","ModeA","hbM","A","space=5");
   zdialog_add_widget(zd,"radio","ModeB","hbM","B","space=5");

   zdialog_add_widget(zd,"hbox","hb4","dialog",0,"space=8");
   zdialog_add_widget(zd,"check","Fbw","hb4","black/white","space=5");
   zdialog_add_widget(zd,"label","space","hb4",0,"space=10");
   zdialog_add_widget(zd,"check","Fneg","hb4","negative");
   
   Blo = 30;
   Bhi = 40;
   Amp = 0;
   ModeA = 1;
   ModeB = 0;
   Fbw = Fneg = 0;

   zdialog_stuff(zd,"Fbw",0);
   zdialog_stuff(zd,"Fneg",0);

   int cc = Eww * Ehh * sizeof(float);
   conmap = (float *) zmalloc(cc,"outlines");                                    //  allocate contrast map
   Fconmap_done = 0;

   zdialog_run(zd,outlines_dialog_event,"save");                                 //  run dialog, parallel
   zdialog_send_event(zd,"init");

   return;
}


//  dialog event and completion callback function

int outlines_dialog_event(zdialog *zd, ch *event)                                //  dialog event function
{
   using namespace outlines_names;

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  commit edit
         edit_addhist("Blo:%.0f Bhi:%.0f Amp:%.0f Mode:%c B&W:%d Neg:%d",
             Blo,Bhi,Amp,'A'+ModeB,Fbw,Fneg);                                    //  edit parms > edit hist
         edit_done(0);
      }
      else edit_cancel(0);                                                       //  discard edit
      
      zfree(conmap);
      return 1;
   }

   zdialog_fetch(zd,"Blo",Blo);                                                  //  get brightness threshold range
   zdialog_fetch(zd,"Bhi",Bhi);
   zdialog_fetch(zd,"Amp",Amp);
   zdialog_fetch(zd,"ModeA",ModeA);
   zdialog_fetch(zd,"ModeB",ModeB);
   zdialog_fetch(zd,"Fbw",Fbw);
   zdialog_fetch(zd,"Fneg",Fneg);
   
   thread_signal();
   
   return 1;
}


//  thread function to update image

void * outlines_thread(void *)
{
   using namespace outlines_names;

   void * outlines_wthread1(void *arg);
   void * outlines_wthread2(void *arg);
   
   if (! Fconmap_done) 
      do_wthreads(outlines_wthread1,NSMP);                                       //  make contrast map

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

   do_wthreads(outlines_wthread2,NSMP);                                          //  draw image

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

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

   return 0;
}


void * outlines_wthread1(void *arg)                                              //  make contrast map
{
   using namespace outlines_names;
   
   int      index = *((int *) arg);
   int      px, py, ii;
   float    *pix1, *pix2;
   float    cont[12];

/***
        get contrast of opposing pixels within 5x5 block centered on pixel P
         P[-2,-2]
              P[0,0]
                  P[+2,+2]
***/

   for (py = index+2; py < Ehh-2; py += NSMP)                                    //  loop all pixels, get contrast
   for (px = 2; px < Eww-2; px++)
   {    
      pix1 = PXMpix(E1pxm,px-2,py-2);
      pix2 = PXMpix(E1pxm,px+2,py+2);
      cont[0] = 1.0 - PIXmatch(pix1,pix2);

      pix1 = PXMpix(E1pxm,px-1,py-2);
      pix2 = PXMpix(E1pxm,px+1,py+2);
      cont[1] = 1.0 - PIXmatch(pix1,pix2);

      pix1 = PXMpix(E1pxm,px+0,py-2);
      pix2 = PXMpix(E1pxm,px+0,py+2);
      cont[2] = 1.0 - PIXmatch(pix1,pix2);

      pix1 = PXMpix(E1pxm,px+1,py-2);
      pix2 = PXMpix(E1pxm,px-1,py+2);
      cont[3] = 1.0 - PIXmatch(pix1,pix2);

      pix1 = PXMpix(E1pxm,px+2,py-2);
      pix2 = PXMpix(E1pxm,px-2,py+2);
      cont[4] = 1.0 - PIXmatch(pix1,pix2);

      pix1 = PXMpix(E1pxm,px-2,py-1);
      pix2 = PXMpix(E1pxm,px+2,py+1);
      cont[5] = 1.0 - PIXmatch(pix1,pix2);

      pix1 = PXMpix(E1pxm,px-1,py-1);
      pix2 = PXMpix(E1pxm,px+1,py+1);
      cont[6] = 1.0 - PIXmatch(pix1,pix2);

      pix1 = PXMpix(E1pxm,px+0,py-1);
      pix2 = PXMpix(E1pxm,px+0,py+1);
      cont[7] = 1.0 - PIXmatch(pix1,pix2);

      pix1 = PXMpix(E1pxm,px+1,py-1);
      pix2 = PXMpix(E1pxm,px-1,py+1);
      cont[8] = 1.0 - PIXmatch(pix1,pix2);

      pix1 = PXMpix(E1pxm,px+2,py-1);
      pix2 = PXMpix(E1pxm,px-2,py+1);
      cont[9] = 1.0 - PIXmatch(pix1,pix2);

      pix1 = PXMpix(E1pxm,px-2,py+0);
      pix2 = PXMpix(E1pxm,px+2,py+0);
      cont[10] = 1.0 - PIXmatch(pix1,pix2);

      pix1 = PXMpix(E1pxm,px-1,py+0);
      pix2 = PXMpix(E1pxm,px+1,py+0);
      cont[11] = 1.0 - PIXmatch(pix1,pix2);

      HeapSort(cont,12);                                                         //  sort values ascending
      
      ii = py * Eww + px;
      conmap[ii] = 0.333 * (cont[9] + cont[10] + cont[11]);                      //  average 3 highest values              24.30
   }

   return 0;
}


void * outlines_wthread2(void *arg)                                              //  draw image                            24.30
{
   using namespace outlines_names;
   
   int      index = *((int *) arg);
   int      px, py, ii, Fend;
   float    *pix1, *pix3;
   float    R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float    blend, contrast, maxrgb, addrgb;
   float    blo, bhi, amp, F1, F2;
   
   blo = 0.01 * Blo;                                                             //  normalize 0 - 1
   bhi = 0.01 * Bhi;
   amp = 0.1 * Amp;
   amp = sqrtf(amp); 

   while (true)                                                                  //  pixel loop
   {
      if (Fescape) break;
      
      Fend = get_edit_pixels(index,px,py,blend);                                 //  next pixel
      if (Fend) break;
      if (blend == 0) continue;
      
      if (px == 0 || px == Eww-1) continue;                                      //  avoid image edges
      if (py == 0 || py == Ehh-1) continue;

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

      R1 = pix1[0];
      G1 = pix1[1];
      B1 = pix1[2];

      R3 = pix3[0];
      G3 = pix3[1];
      B3 = pix3[2];
      
      R9 = R1;
      G9 = G1;
      B9 = B1;

      ii = py * Eww + px;
      contrast = conmap[ii];                                                     //  pixel contrast value
      
      if (contrast < blo) {                                                      //  pixel contrast below range
         R9 = G9 = B9 = 0;                                                       //  brightness = 0
         goto paint;
      }

      else if (contrast > bhi) F1 = 1.0;                                         //  above range, brightness = max.
      
      else F1 = (contrast - blo) / (bhi - blo);                                  //  get relative brightness, 0 - 1
      
      F1 += amp * (1.0 - F1);                                                    //  amplify dim pixel                     24.40
      
      maxrgb = R9;                                                               //  get max. pixel RGB value
      if (G9 > maxrgb) maxrgb = G9;
      if (B9 > maxrgb) maxrgb = B9;

      if (Fbw) R9 = G9 = B9 = maxrgb;                                            //  make black/white 

      addrgb = 255 - maxrgb;                                                     //  amount poss. to brighten
      if (addrgb < 1 || addrgb > 254) continue;
      
      F2 = 255.0 / maxrgb;                                                       //  maximize brightness                   24.40
      if (ModeB) F2 = 1.0;                                                       //  use flat method                       24.40
      
      R9 = F1 * F2 * R9;                                                         //  brighten pixel proportionally
      G9 = F1 * F2 * G9;
      B9 = F1 * F2 * B9;
      
   paint:
      
      if (Fneg)                                                                  //  make negative
      {
         R9 = 255 - R9;
         G9 = 255 - G9;
         B9 = 255 - B9;
      }

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

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

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

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

   return 0;
}


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

//  sketch menu function - convert photo to simulated sketch

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


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

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

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

   F1_help_topic = "sketch";

   Plog(1,"m_sketch \n");

   EFsketch.menuname = "Sketch";
   EFsketch.menufunc = m_sketch;
   EFsketch.Farea = 2;                                                           //  select area usable
   EFsketch.Fscript = 1;                                                         //  scripting supported
   EFsketch.threadfunc = sketch_thread;                                          //  thread function

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

   cc = Eww * Ehh;

   britness = (uint8 *) zmalloc(cc,"sketch");
   contrast = (uint8 *) zmalloc(cc,"sketch");
   monopix = (uint8 *) zmalloc(cc,"sketch");
   fclip = (uint8 *) zmalloc(cc,"sketch");

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

   for (py = 1; py < Ehh-1; py++)                                                //  precalculate pixel contrast
   for (px = 1; px < Eww-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 * Eww + px;
      contrast[ii] = 255.0 * (1.0 - f1 * f2);                                    //  pixel color contrast 0-255
   }

/***
        _________________________________________
       |        Convert to Sketch                |
       |                                         |
       |  Brightness ==============[]==========  |
       |  Threshold ======[]===================  |
       |  Contrast =========[]=================  |
       |  Clip Level ===================[]=====  |
       |  Foreground [#####]  Background [#####] |
       |                                         |
       |                                [OK] [X] |
       |_________________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,"OK"," X ",null);                        //  sketch dialog
   EFsketch.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labbrite","hb1","Brightness","space=5");
   zdialog_add_widget(zd,"hscale2","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","Threshold","space=5");
   zdialog_add_widget(zd,"hscale2","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","Contrast","space=5");
   zdialog_add_widget(zd,"hscale2","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","Clip Level","space=5");
   zdialog_add_widget(zd,"hscale2","cliplev","hb4","0|255|1|0","expand|space=3");

   zdialog_add_widget(zd,"hbox","hb6","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labfg","hb6","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","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;

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

   return;
}


//  sketch dialog event and completion function

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

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

   if (zd->zstat)
   {
      if (zd->zstat == 1 && ! Fescape) {                                         //  commit edit
         edit_addhist("Br:%.3f Th:%d Con:%.3f Clip:%d",
                    Bweight,Brthresh,Cweight,cliplev);                           //  edit parms > edit hist
         edit_done(0);
      }
      else edit_cancel(0);                                                       //  discard edit
      zfree(britness);
      zfree(contrast);
      zfree(monopix);
      zfree(fclip);
      return 1;
   }

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

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

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

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

   thread_signal();

   return 1;
}


//  thread function - multiple working threads to update image

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

   void sketch_wthread();

   progress_setgoal(255);
   sketch_wthread();
   progress_setgoal(0);
   
   CEF->Fmods++;                                                                 //  image modified
   CEF->Fsaved = 0;

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


//  working thread

void sketch_wthread()
{
   using namespace sketch_names;

   int         px, py, qx, qy;
   int         ii, jj, kk, dist;
   int         br0, br1, br2, br3, move;
   int         bright, cont, brcon, brsum;
   int         R, G, B;
   float       *pix3, f1, f2;

   for (py = 0; py < Ehh; py++)                                                  //  convert to monocolor image
   for (px = 0; px < Eww; px++)                                                  //    with brightness range 0-255
   {
      if (Fescape) return;                                                       //  user kill

      ii = py * Eww + px;

      if (sa_stat == sa_stat_fini) {                                             //  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 = contrast[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 < Ehh-1; py++)                                             //  loop image interior pixels
      for (px = 1; px < Eww-1; px++)
      {
         if (Fescape) return;                                                    //  user kill

         ii = py * Eww + px;

         if (sa_stat == sa_stat_fini) {                                          //  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 * Eww + 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 * Eww + 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: ;
      }
      
      progress_addvalue(50);
   }

   memset(fclip,0,cc);

   for (py = 1; py < Ehh-1; py++)                                                //  loop all pixels
   for (px = 1; px < Eww-1; px++)                                                //  (interior pixels only)
   {
      if (Fescape) return;                                                       //  user kill

      ii = py * Eww + px;

      if (sa_stat == sa_stat_fini) {                                             //  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 * Eww + 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 < Ehh; py++)                                                  //  loop all pixels
   for (px = 0; px < Eww; px++)
   {
      if (Fescape) return;                                                       //  user kill

      ii = py * Eww + 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

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

   return;
}


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

//  convert an image into a cartoon drawing

namespace cartoon
{
   editfunc    EFcartoon;
   int         line_threshold = 100;
   int         line_width = 3;
   int         blur_radius = 4;
   int         kuwahara_depth = 2;
   int         priorKD, priorBR;
   float       *pixcon;
   float       Fthreshold;
   float       Wrad[41][41];                                                     //  radius <= 20
}


//  menu function

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

   int cartoon_dialog_event(zdialog *zd, ch *event);
   void * cartoon_thread(void *);

   F1_help_topic = "cartoon";

   Plog(1,"m_cartoon \n");

   EFcartoon.menuname = "Cartoon";
   EFcartoon.menufunc = m_cartoon;
   EFcartoon.Farea = 2;                                                          //  select area usable
   EFcartoon.Fscript = 1;                                                        //  scripting supported
   EFcartoon.threadfunc = cartoon_thread;                                        //  thread function

   if (! edit_setup(EFcartoon)) return;                                          //  setup edit
   
   pixcon = (float *) zmalloc(Eww * Ehh * sizeof(float),"cartoon");
   
   E8pxm = PXM_copy(E1pxm);                                                      //  blurred image
   E9pxm = PXM_copy(E1pxm);                                                      //  kuwahara image

/***
       _____________________________
      |         Cartoon             |
      |                             |
      |  Line Threshold  [ 20 ]     |
      |   Line Width     [  3 ]     |
      |  Kuwahara Depth  [  4 ]     |
      |                             |
      |            [Apply] [OK] [X] |
      |_____________________________|
      
***/

   zdialog *zd = zdialog_new("Cartoon",Mwin,"Apply","OK"," X ",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","lab1","vb1","Kuwahara Depth");

   zdialog_add_widget(zd,"zspin","line_threshold","vb2","0|200|1|100");
   zdialog_add_widget(zd,"zspin","line_width","vb2","0|10|1|1");
   zdialog_add_widget(zd,"zspin","kuwahara_depth","vb2","0|10|1|1");
   
   zdialog_resize(zd,200,0);
   zdialog_load_inputs(zd);
   zdialog_run(zd,cartoon_dialog_event,"save");                                  //  run dialog, parallel

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


//  dialog event and completion callback function

int cartoon_dialog_event(zdialog *zd, ch *event)                                 //  dialog event function
{
   using namespace cartoon;

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

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

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

      if (zd->zstat == 2) {                                                      //  [OK]
         edit_addhist("line thresh:%d width:%d blur rad:%d Kuwa depth:%d",
                    line_threshold,line_width,blur_radius,kuwahara_depth);       //  edit parms > edit hist
         edit_done(0);
      }

      else edit_cancel(0);

      zfree(pixcon);
   }

   return 1;
}


//  thread function to update image

void * cartoon_thread(void *)
{
   using namespace cartoon;
   
   void cartoon_blur();
   void cartoon_kuwahara();
   void cartoon_conmap();
   void cartoon_drawlines();

   int      Fblur, Fkuwa, Fcon;
   
   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;
   
   progress_setgoal((Fblur*Fkuwa*Fcon+1)*Eww*Ehh);

   if (Fblur) cartoon_blur();                                                    //  E1 + blur >> E8
   if (Fkuwa) cartoon_kuwahara();                                                //  E8 + kuwahara >> E9
   if (Fcon) cartoon_conmap();                                                   //  compute E9 pixel contrast
   cartoon_drawlines();                                                          //  draw lines where contrast > threshold
   
   progress_setgoal(0);

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

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


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

void cartoon_blur()
{
   using namespace cartoon;

   void * cartoon_blur_wthread(void *arg);

   int      rad, px, py, qx, qy;
   float    R, W;
   
   PXM_free(E8pxm);
   E8pxm = PXM_copy(E1pxm);

   if (blur_radius == 0) return;

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

   return;
}


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

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

   for (py = index + rad; py < Ehh-rad; py += NSMP)                              //  loop all image pixels
   for (px = rad; px < Eww-rad; px++)
   {
      if (Fescape) break;

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

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

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

   return 0;
}


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

void cartoon_kuwahara()
{
   using namespace cartoon;

   void * cartoon_kuwahara_wthread(void *arg);

   PXM_free(E9pxm);
   E9pxm = PXM_copy(E8pxm);
   
   if (kuwahara_depth == 0) return;

   do_wthreads(cartoon_kuwahara_wthread,NSMP);                                   //  worker threads

   return;
}


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

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

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

   for (py = index + rad; py < Ehh-rad; py += NSMP)                              //  loop all image pixels
   for (px = rad; px < Eww-rad; px++)
   {
      if (Fescape) break;

      if (sa_stat == sa_stat_fini) {                                             //  select area active
         ii = py * Eww + 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;
         }
      }

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

   return 0;
}


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

void cartoon_conmap()
{
   using namespace cartoon;

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

   for (py = 1; py < Ehh-1; py++)                                                //  make image pixel contrast map
   for (px = 1; px < Eww-1; px++)
   {
      if (Fescape) break;

      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 * Eww + px;
      pixcon[ii] = pix1con = 0.0417 * sumcon;                                    //  / (8 * 3) 
      if (pix1con > maxcon) maxcon = pix1con;
      
      progress_addvalue(1);
   }
   
   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()
{
   void * cartoon_drawlines_wthread(void *arg);

   PXM_free(E3pxm);
   E3pxm = PXM_copy(E9pxm);
   do_wthreads(cartoon_drawlines_wthread,NSMP);                                  //  worker threads
}


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

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

   for (py = index+1; py < Ehh-1; py += NSMP)
   for (px = 1; px < Eww-1; px++)
   {
      if (Fescape) break;

      ii = py * Eww + px;

      if (sa_stat == sa_stat_fini) {                                             //  select area active
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel outside area
      }
      
      if (pixcon[ii] < Fthresh) continue;
      
      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];
            }
         }
      }

      progress_addvalue(index,1);
   }

   return 0;
}


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

//  convert image to simulate an embossing

namespace emboss_names
{
   editfunc    EFemboss;
   int         radius, depth;
   int         angle = 0;                                                        //  illumination angle
   float       kernel[41][41];                                                   //  support radius <= 20
}


//  menu function

void m_emboss(GtkWidget *, ch *menu)
{
   using namespace emboss_names;
   
   int    emboss_dialog_event(zdialog* zd, ch *event);
   void * emboss_thread(void *);

   F1_help_topic = "emboss";

   Plog(1,"m_emboss \n");

   EFemboss.menuname = "Emboss";
   EFemboss.menufunc = m_emboss;
   EFemboss.Fscript = 1;                                                         //  scripting supported
   EFemboss.Farea = 2;                                                           //  select area usable
   EFemboss.Fpaintedits = 1;                                                     //  can use paint edits
   EFemboss.threadfunc = emboss_thread;                                          //  thread function

   if (! edit_setup(EFemboss)) return;                                           //  setup edit
   
/***
          __________________________
         |         Emboss           |
         |                          |
         | Radius [__]  Depth [__]  |
         | Light Source [___] deg.  |
         |                          |
         |                 [OK] [X] |
         |__________________________|

***/

   zdialog *zd = zdialog_new("Emboss",Mwin,"OK"," X ",null);
   EFemboss.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1","Radius","space=5");
   zdialog_add_widget(zd,"zspin","radius","hb1","0|20|1|2");
   zdialog_add_widget(zd,"label","lab2","hb1","Depth","space=5");
   zdialog_add_widget(zd,"zspin","depth","hb1","0|99|1|3");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab2","hb2","Light Source","space=5");
   zdialog_add_widget(zd,"zspin","angle","hb2","0|360|1|0");
   zdialog_add_widget(zd,"label","lab3","hb2","deg.","space=5");
   zdialog_add_ttip(zd,"angle","direction of light and shadows"); 
   
   radius = 2;
   depth = 3;
   angle = 0;

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


//  dialog event and completion callback function

int emboss_dialog_event(zdialog *zd, ch *event)                                  //  emboss dialog event function
{
   using namespace emboss_names;
   
   if (strmatch(event,"focus")) return 1;
   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()

   if (strmatch(event,"apply")) {                                                //  from script                           24.30
      zdialog_fetch(zd,"radius",radius);                                         //  get user inputs
      zdialog_fetch(zd,"depth",depth);
      zdialog_fetch(zd,"angle",angle);
      thread_signal();
      zd->zstat = 1;
   }


   if (zd->zstat) {
      if (zd->zstat == 1) {                                                      //  [OK] commit edit
         edit_addhist("radius:%d depth:%d angle:%d",radius,depth,angle);         //  edit parms > edit hist
         edit_done(0);
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }
   
   if (zstrstr("paint radius depth angle",event)) {                              //  mouse paint or dialog input
      zdialog_fetch(zd,"radius",radius);                                         //  get user inputs
      zdialog_fetch(zd,"depth",depth);
      zdialog_fetch(zd,"angle",angle);
      thread_signal();
   }

   return 1;
}


//  thread function - use multiple working threads

void * emboss_thread(void *)
{
   using namespace emboss_names;
   
   void  * emboss_wthread(void *arg);

   int      dx, dy, rad;
   float    C1, C2, R;
   float    T = angle / 57.3;                                                    //  illumination direction
   float    Cx = sinf(T), Cy = cosf(T); 

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

   rad = radius;

   C1 = 0.1 * depth / (rad * rad + 1);                                           //  max. illumination
   
   for (dy = -rad; dy <= rad; dy++)                                              //  build kernel with radius and depth
   for (dx = -rad; dx <= rad; dx++)
   {
      if (dx == 0 && dy == 0) continue;
      R = sqrtf(dx*dx + dy*dy);                                                  //  cell distance from kernel center
      C2 = C1 * (Cx*dx + Cy*dy)/R;                                               //  illumination of cell
      kernel[dx+rad][dy+rad] = C2;
   }
      
   kernel[rad][rad] = 1;                                                         //  kernel center cell = 1

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

   do_wthreads(emboss_wthread,NSMP);                                             //  worker threads

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

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


void * emboss_wthread(void *arg)                                                 //  worker thread function
{
   using namespace emboss_names;
   
   int         index = *((int *) (arg));
   int         px, py, Fend;
   int         dx, dy, rad;
   int         nc = E1pxm->nc;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       bright1, bright3;
   float       *pix1, *pix3, *pixN;
   float       sumpix, kern;
   float       blend;

   rad = radius;

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

      if (px < rad || py < rad) continue;
      if (px > Eww-rad-1 || py > Ehh-rad-1) continue;

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

      R1 = pix1[0];
      G1 = pix1[1];
      B1 = pix1[2];

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

      sumpix = 0;

      for (dy = -rad; dy <= rad; dy++)                                           //  loop surrounding block of pixels
      for (dx = -rad; dx <= rad; dx++)
      {
         pixN = pix1 + (dy * Eww + dx) * nc;
         kern = kernel[dx+rad][dy+rad];
         sumpix += kern * (pixN[0] + pixN[1] + pixN[2]);                         //  kern sums to 1.0
      }

      bright1 = pix1[0] + pix1[1] + pix1[2] + 1;
      bright3 = (sumpix + 1) / bright1;                                          //  ratio of brightness change
      
      R9 = bright3 * R1;
      G9 = bright3 * G1;
      B9 = bright3 * B1;
      
      RGBfix(R9,G9,B9); 

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

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

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

   return 0;
}


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

//  convert image to simulate square tiles

namespace tiles1_names
{
   editfunc    EFtiles1;
   int         size, gap, D3;
   float       *pixmap = 0;
}


void m_tiles1(GtkWidget *, ch *menu)
{
   using namespace tiles1_names;

   int    tiles1_dialog_event(zdialog *zd, ch *event);
   void * tiles1_thread(void *);
   int64  z64 = 1;

   F1_help_topic = "tiles1";

   Plog(1,"m_tiles1 \n");

   EFtiles1.menuname = "Tiles1";
   EFtiles1.menufunc = m_tiles1;
   EFtiles1.Farea = 2;                                                           //  select area usable
   EFtiles1.Fscript = 1;                                                         //  scripting supported
   EFtiles1.threadfunc = tiles1_thread;                                          //  thread function

   if (! edit_setup(EFtiles1)) return;                                           //  setup edit
   
/***
       _____________________________
      |     Make Square Tiles       |
      |                             |
      | tile size [____]            |
      | tile gap  [____]            |
      | 3D depth  [____]            |
      |                             |
      |            [Apply] [OK] [X] |
      |_____________________________|

***/

   zdialog *zd = zdialog_new("Make Square Tiles",Mwin,"Apply","OK"," X ",null);
   EFtiles1.zd = zd;

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

   size = 10;                                                                    //  defaults
   gap = 1;
   D3 = 0;

   int64 cc = z64 * Eww * Ehh * 3 * sizeof(float);
   pixmap = (float *) zmalloc(cc,"tiles1");                                      //  set up pixel color map

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


//  dialog event and completion callback function

int tiles1_dialog_event(zdialog * zd, ch *event)
{
   using namespace tiles1_names;

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

   if (zd->zstat)                                                                //  dialog complete
   {
      if (zd->zstat == 1) {                                                      //  apply
         zd->zstat = 0;                                                          //  keep dialog active
         zdialog_fetch(zd,"size",size);                                          //  get tile size
         zdialog_fetch(zd,"gap",gap);                                            //  get tile gap
         zdialog_fetch(zd,"D3",D3);                                              //  get 3D yes/no
         if (size < 2) edit_reset();                                             //  restore original image
         else thread_signal();                                                   //  trigger working thread
         return 1;
      }
      else if (zd->zstat == 2) {                                                 //  commit edit
         edit_addhist("tile size:%d gap:%d depth:%d",size,gap,D3);               //  edit parms > edit hist
         edit_done(0);
      }
      else edit_cancel(0);                                                       //  discard edit
      zfree(pixmap);
   }

   return 1;
}


//  image tiles1 thread function

void * tiles1_thread(void *)
{
   using namespace tiles1_names;

   int         sg, gg, eg, sumpix;
   int         ii, px, py, qx, qy;
   int         td, bd, ld, rd;
   int64       zii, z64 = 1;
   float       red = 0, green = 0, blue = 0;
   float       hired = 0, higreen = 0, hiblue = 0;
   float       lored = 0, logreen = 0, loblue = 0;
   float       *pix1, *pix3;

   sg = size + gap;
   gg = gap;

   for (py = 0; py < Ehh; py += sg)                                              //  initz. pixel color map for
   for (px = 0; px < Eww; 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 > Ehh-1 || qx > Eww-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 (D3)
      {
         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 > Ehh-1 || qx > Eww-1) continue;

         zii = z64 * (qy * Eww + 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
            pixmap[zii] = pixmap[zii+1] = pixmap[zii+2] = 0;
            continue;
         }

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

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

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

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

         pixmap[zii] = red;
         pixmap[zii+1] = green;
         pixmap[zii+2] = blue;
      }
   }

   if (sa_stat == sa_stat_fini)                                                  //  process selected area
   {
      for (ii = 0; ii < Eww * Ehh; ii++)                                         //  find pixels in select area
      {
         if (! sa_pixmap[ii]) continue;
         py = ii / Eww;
         px = ii - py * Eww;
         pix3 = PXMpix(E3pxm,px,py);
         zii = z64 * (py * Eww + px) * 3;
         pix3[0] = pixmap[zii];
         pix3[1] = pixmap[zii+1];                                                //  apply block color to member pixels
         pix3[2] = pixmap[zii+2];
      }
   }

   else                                                                          //  process entire image
   {
      for (py = 0; py < Ehh-1; py++)                                             //  loop all image pixels
      for (px = 0; px < Eww-1; px++)
      {
         pix3 = PXMpix(E3pxm,px,py);                                             //  target pixel
         zii = z64 * (py * Eww + px) * 3;                                        //  color map for (px,py) 
         pix3[0] = pixmap[zii];
         pix3[1] = pixmap[zii+1];
         pix3[2] = pixmap[zii+2];
      }
   }

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

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


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

//  common functions for m_tiles2() and m_painting()
//  pixgroups_make
//  pixgroups_merge
//  pixgroups_borders

namespace pixgroups_names
{
   int         color_depth;
   int         area_goal;
   int         color_match;

   typedef struct  {
      int16       px, py;
      ch          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
   float       *pixmap = 0;

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


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

void pixgroups_make()
{
   using namespace pixgroups_names;

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

   cc1 = Eww * Ehh;

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

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

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

   group = 0;

   for (py = 0; py < Ehh; py++)                                                  //  loop all pixels
   for (px = 0; px < Eww; px++)
   {
      if (Fescape) break;
      
      if (sa_stat == sa_stat_fini) {                                             //  select area active
         ii = py * Eww + px;
         if (sa_pixmap[ii] == 0) continue;                                       //  pixel outside of area
      }

      kk = py * Eww + px;
      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)
      {
         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 > Eww-1) continue;                                   //  pixel off the edge
         if (npy < 0 || npy > Ehh-1) continue;

         kk = npy * Eww + npx;

         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 pixgroups_merge()
{
   using namespace pixgroups_names;

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

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

   snewgroup      *newgroup;

   nccc = z64 * (group + 1) * sizeof(snewgroup);                                 //  group count
   newgroup = (snewgroup *) zmalloc(nccc,"painting");

   while (true)
   {
      memset(newgroup,0,nccc);

      for (py = 0; py < Ehh; py++)                                               //  loop all pixels
      for (px = 0; px < Eww; px++)
      {
         if (Fescape) break;

         kk = Eww * py + px;                                                     //  get assigned group
         group = pixgroup[kk];
         if (! group) continue;
         if (groupcount[group] >= area_goal) 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 >= Eww) continue;                                 //  off the edge
            if (npy < 0 || npy >= Ehh) continue;

            kk = Eww * 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 < colrmatch) 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 < Ehh; py++)                                               //  loop all pixels
      for (px = 0; px < Eww; px++)
      {
         if (Fescape) break;

         kk = Eww * py + px;
         group = pixgroup[kk];                                                   //  test for new group assignment
         if (! group) continue;
         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 matching pixels

void pixgroups_borders()
{
   using namespace pixgroups_names;

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

   cc = Eww * Ehh;
   ch * pixblack = (ch *) zmalloc(cc,"painting");

   for (py = 1; py < Ehh; py++)                                                  //  loop all pixels
   for (px = 1; px < Eww; px++)                                                  //  omit top and left
   {
      if (Fescape) break;

      if (sa_stat == sa_stat_fini) {                                             //  select area active
         ii = py * Eww + px;
         if (sa_pixmap[ii] == 0) continue;                                       //  pixel outside of area
      }

      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 = Eww * 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 = Eww * (py-1) + px;                                                 //  have vertical transition
         if (pixblack[kk]) continue;
         kk += Eww;
         pixblack[kk] = 1;
      }
   }

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

   zfree(pixblack);
   return;
}


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

//  convert image to irregular tiles matching image features

namespace tiles2_names
{
   int         base_size;
   editfunc    EFtiles2;
}


void m_tiles2(GtkWidget *, ch *menu)
{
   using namespace tiles2_names;
   using namespace pixgroups_names;

   int    tiles2_dialog_event(zdialog *zd, ch *event);
   void * tiles2_thread(void *);

   F1_help_topic = "tiles2";

   Plog(1,"m_tiles2 \n");

   EFtiles2.menuname = "Tiles2";
   EFtiles2.menufunc = m_tiles2;
   EFtiles2.Farea = 2;                                                           //  select area usable
   EFtiles2.Fscript = 1;                                                         //  scripting supported
   EFtiles2.threadfunc = tiles2_thread;                                          //  thread function

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

/***
       _____________________________
      |       Irregular Tiles       |
      |                             |
      |  base size [____]           |
      |  color depth [___]          |
      |  tile area goal [____]      |
      |  req. color match [___]     |
      |                             |
      |            [Apply] [OK] [X] |
      |_____________________________|

***/

   zdialog *zd = zdialog_new("Irregular Tiles",Mwin,"Apply","OK"," X ",null);
   EFtiles2.zd = zd;

   zdialog_add_widget(zd,"hbox","hbsize","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labsize","hbsize","base size","space=5");
   zdialog_add_widget(zd,"zspin","base_size","hbsize","2|99|1|10","space=5");

   zdialog_add_widget(zd,"hbox","hbcolor","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labcolor","hbcolor","color depth","space=5");
   zdialog_add_widget(zd,"zspin","color_depth","hbcolor","1|8|1|4","space=5");

   zdialog_add_widget(zd,"hbox","hbgoal","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labgoal","hbgoal","tile area goal","space=5");
   zdialog_add_widget(zd,"zspin","area_goal","hbgoal","0|99999|100|1000","space=5");

   zdialog_add_widget(zd,"hbox","hbmatch","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labmatch","hbmatch","req. color match","space=5");
   zdialog_add_widget(zd,"zspin","color_match","hbmatch","0|99|1|80","space=5");

   base_size = 10;
   color_depth = 4;
   area_goal = 1000;
   color_match = 80;

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


//  dialog event and completion callback function

int tiles2_dialog_event(zdialog * zd, ch *event)
{
   using namespace tiles2_names;
   using namespace pixgroups_names;

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

   if (zd->zstat)                                                                //  dialog complete
   {
      if (zd->zstat == 1) {                                                      //  apply
         zd->zstat = 0;                                                          //  keep dialog active
         zdialog_fetch(zd,"base_size",base_size);                                //  get tile base size
         zdialog_fetch(zd,"color_depth",color_depth);                            //  get tile color depth
         zdialog_fetch(zd,"area_goal",area_goal);                                //  group area goal
         zdialog_fetch(zd,"color_match",color_match);                            //  req. color match to combine groups
         thread_signal();                                                        //  trigger working thread
         return 1;
      }
      else if (zd->zstat == 2) {                                                 //  commit edit
         edit_addhist("base size:%d",base_size);                                 //  edit parms > edit hist
         edit_done(0);
      }
      else edit_cancel(0);                                                       //  discard edit
   }

   return 1;
}


//  tiles2 thread function

void * tiles2_thread(void *) 
{
   using namespace tiles2_names;
   using namespace pixgroups_names;

   void tiles2_base();                                                           //  color base tiles
   void tiles2_colordepth();                                                     //  reduce tiles color depth

   tiles2_base();
   tiles2_colordepth();
   pixgroups_make();
   pixgroups_merge();
   pixgroups_borders();
   sa_postfix();                                                                 //  if select area, restore unselected

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

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


//  form base tiles with mean colors from input image

void tiles2_base()
{
   using namespace tiles2_names;
   using namespace pixgroups_names;

   int         bsz, sumpix;
   int         px, py, qx, qy;
   float       red, green, blue;
   float       *pix1, *pix3;

   bsz = base_size;

   for (py = 0; py < Ehh - bsz; py += bsz)                                       //  initz. pixel color map for
   for (px = 0; px < Eww - bsz; px += bsz)                                       //    given pixel size
   {
      if (Fescape) break;

      sumpix = red = green = blue = 0;

      for (qy = py; qy < py + bsz; qy++)                                         //  get mean color for pixel block
      for (qx = px; qx < px + bsz; qx++)
      {
         pix1 = PXMpix(E1pxm,qx,qy);
         red += pix1[0];
         green += pix1[1];
         blue += pix1[2];
         sumpix++;
      }

      if (! sumpix) continue;                                                    //  black

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

      for (qy = py; qy < py + bsz; qy++)                                         //  set color for pixels in block
      for (qx = px; qx < px + bsz; qx++)
      {
         pix3 = PXMpix(E3pxm,qx,qy);
         pix3[0] = red;
         pix3[1] = green;
         pix3[2] = blue;
      }
   }

   return;
}


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

void tiles2_colordepth()
{
   using namespace pixgroups_names;

   PXM_rgbdepth(E3pxm,color_depth);

   return;
}


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

//  convert image to simulate a painting

namespace painting_names
{
   editfunc    EFpainting;
   int         blur_radius;
   int         borders;
}


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

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

   F1_help_topic = "painting";

   Plog(1,"m_painting \n");

   EFpainting.menuname = "Painting";
   EFpainting.menufunc = m_painting;
   EFpainting.Farea = 2;                                                         //  select area usable
   EFpainting.Fscript = 1;                                                       //  scripting supported
   EFpainting.threadfunc = painting_thread;                                      //  thread function

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

/***
       _____________________________
      |    Simulate Painting        |
      |                             |
      |  blur radius [____]         |
      |  color depth [____]         |
      |  patch area goal [____]     |
      |  req. color match [____]    |
      |  borders [_]                |
      |                             |
      |            [Apply] [OK] [X] |
      |_____________________________|
      
***/

   zdialog *zd = zdialog_new("Painting",Mwin,"Apply","OK"," X ",null);
   EFpainting.zd = zd;

   zdialog_add_widget(zd,"hbox","hbbr","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labbr","hbbr","blur radius","space=5");
   zdialog_add_widget(zd,"zspin","blurrad","hbbr","0|20|1|3","space=5");

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

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

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

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

   zdialog_load_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, ch *event)
{
   using namespace painting_names;
   using namespace pixgroups_names;

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

   if (zd->zstat)
   {  
      if (zd->zstat == 1) {                                                      //  apply
         zd->zstat = 0;                                                          //  keep dialog active
         edit_reset();                                                           //  start over
         zdialog_fetch(zd,"blurrad",blur_radius);                                //  blur radius
         zdialog_fetch(zd,"colordepth",color_depth);                             //  color depth
         zdialog_fetch(zd,"area_goal",area_goal);                                //  target pixel group area
         zdialog_fetch(zd,"colormatch",color_match);                             //  req. color match to combine groups
         zdialog_fetch(zd,"borders",borders);                                    //  borders wanted
         thread_signal();                                                        //  do the work
      }
      else if (zd->zstat == 2) {                                                 //  done, commit edit
         edit_addhist("color:%d patch:%d match:%d borders:%d",
                           color_depth,area_goal,color_match,borders);           //  edit parms > edit hist
         edit_done(0);
      }

      else edit_cancel(0);

      return 1;
   }
   
   return 0;
}


//  painting thread function

void * painting_thread(void *)
{
   using namespace painting_names;

   void  painting_blur();
   void  painting_colordepth();

   painting_blur();                                                              //  blur image 
   painting_colordepth();                                                        //  set new color depth
   pixgroups_make();
   pixgroups_merge();
   if (borders) pixgroups_borders();
   sa_postfix();                                                                 //  if select area, restore unselected

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

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


//  blur the image using specified blur radius

void painting_blur()
{
   using namespace painting_names;

   PXM_blur(E3pxm,blur_radius);

   return;
}


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

void painting_colordepth() 
{
   using namespace pixgroups_names;

   PXM_rgbdepth(E3pxm,color_depth);

   return;
}


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

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

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

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

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

   zdialog  *zd;

   F1_help_topic = "texture";

   Plog(1,"m_texture \n");

   EFtexture.menuname = "Texture";
   EFtexture.menufunc = m_texture;
   EFtexture.Farea = 2;                                                          //  select area OK
   EFtexture.Fpaintedits = 1;                                                    //  can use paint edits
   EFtexture.Fscript = 1;                                                        //  scripting supported
   EFtexture.threadfunc = texture_thread;                                        //  thread function

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

/***
       ___________________________________
      |          Add Texture              |
      |                                   |
      |  Radius [___]  Power [___]        |
      |                                   |
      |                  [Apply] [OK] [X] |
      |___________________________________|

***/

   zd = zdialog_new("Add Texture",Mwin,"Apply","OK"," X ",null);                 //  texture dialog
   CEF->zd = zd;

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

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

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


//  texture dialog event and completion function

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

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  apply
         zd->zstat = 0;                                                          //  keep dialog active
         edit_reset();
         zdialog_fetch(zd,"radius",radius);
         zdialog_fetch(zd,"power",power);
         thread_signal();                                                        //  process
         return 1;
      }

      if (zd->zstat == 2) {                                                      //  commit edit
         edit_addhist("radius:%d power:%d",radius,power);                        //  edit parms > edit hist
         edit_done(0);
         return 1;
      }

      edit_cancel(0);                                                            //  cancel edit
      return 1;
   }

   if (strmatch(event,"paint")) {                                                //  mouse paint
      zdialog_fetch(zd,"radius",radius);
      zdialog_fetch(zd,"power",power);
      thread_signal();
   }
   
   return 1;
}


//  thread function - update image

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

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

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

   if (sa_stat == sa_stat_fini) progress_setgoal(sa_Npixel);                     //  initz. progress counter
   else  progress_setgoal(Eww * Ehh);

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

   do_wthreads(texture_wthread,NSMP);                                            //  worker threads

   progress_setgoal(0);

   CEF->Fmods = 1;                                                               //  image modified
   CEF->Fsaved = 0;                                                              //  not saved
   
   if (! Fpaintedits) Fpaint2();                                                 //  update window
   return 0;
}


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

   int         index = *((int *) (arg));
   int         px, py, qx, qy, npix;
   int         Fend, rank1, rank2;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       *pix1, *pix3, *qpix;
   float       pow, ff;
   float       pbright, qbright;
   float       blend;

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

   while (true)
   {
      if (Fescape) break;
      
      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) break;
      if (blend == 0) continue;
      
      if (px < radius || px >= Eww-radius) continue;                             //  avoid image edges
      if (py < radius || py >= Ehh-radius) continue;

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

      R1 = pix1[0];
      G1 = pix1[1];
      B1 = pix1[2];

      R3 = pix3[0];
      G3 = pix3[1];
      B3 = pix3[2];
      
      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

      R9 = ff * R1;                                                              //  RGB for new brightness level
      G9 = ff * G1;
      B9 = ff * B1;

      RGBfix(R9,G9,B9);

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

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

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

      progress_addvalue(index,1);
   }

   return 0;                                                                     //  exit thread
}


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

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

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


//  menu function

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

   int   pattern_dialog_event(zdialog* zd, ch *event);

   zdialog  *zd;

   F1_help_topic = "pattern";

   Plog(1,"m_pattern \n");

   EFpattern.menuname = "Pattern";
   EFpattern.menufunc = m_pattern;
   EFpattern.Farea = 2;                                                          //  select area OK
   EFpattern.Fscript = 1;                                                        //  scripting supported

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

/***
       _______________________________________________
      |            Background Pattern                 |
      |                                               |
      |  Pattern File [____________________] [Browse] |
      |  Zoom [___]  Geometry [Calculate]             |
      |  Pattern Width NNN   Height NNN               |                          //  labels not spin buttons 
      |  Overlap Width [___]  Height [___]            |
      |  Opacity [___]  Contrast [___]  [x] Grayscale |
      |                                               |
      |                              [Apply] [OK] [X] |
      |_______________________________________________|

***/

   zd = zdialog_new("Background Pattern",Mwin,"Apply","OK"," X ",null);
   CEF->zd = zd;

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

   zdialog_add_widget(zd,"hbox","hbcalc","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labzoom","hbcalc","Zoom","space=5");
   zdialog_add_widget(zd,"zspin","zoom","hbcalc","1.0|5.0|0.01|1.0");
   zdialog_add_widget(zd,"label","space","hbcalc",0,"space=10");
   zdialog_add_widget(zd,"zbutton","calc","hbcalc","Calculate Geometry","space=3");

   zdialog_add_widget(zd,"hbox","hbs11","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labpattw","hbs11","Pattern Width","space=5");
   zdialog_add_widget(zd,"label","pattww","hbs11","tbd","space=5");
   zdialog_add_widget(zd,"label","space","hbs11",0,"space=10");
   zdialog_add_widget(zd,"label","labheight","hbs11","Height","space=3");
   zdialog_add_widget(zd,"label","patthh","hbs11","tbd","space=5");

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

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

   zdialog_load_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, ch *event)
{
   using namespace patternnames;

   void  pattern_match();
   void  pattern_image();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

   return 1;
}


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

void pattern_match()
{
   using namespace patternnames;

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

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

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

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

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

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

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

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

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

   return;
}


//  combine image and pattern

void pattern_image()
{
   using namespace patternnames;

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

   cc = Eww * Ehh;                                                               //  tile pixels mapping to image pixel
   tmap = (ch *) zmalloc(cc,"pattern");

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

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

   Ntrows = Ehh / (phh-2*olaphh) + 1;                                            //  tile rows and columns including
   Ntcols = Eww / (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 >= Ehh || mpx >= Eww) continue;
         ii = mpy * Eww + 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 >= Ehh || mpx >= Eww) continue;
         ii = mpy * Eww + mpx;
         pixt = pixels + tpy * prs + tpx * 3;                                    //  input tile pixel
         R = pixt[0];
         G = pixt[1];
         B = pixt[2];
         if (Fgray) R = G = B = 0.333 * (R + G + B);                             //  convert to grayscale
         pix3 = PXMpix(E3pxm,mpx,mpy);                                           //  output image pixel
         pix3[0] += (0.01 * opacity / tmap[ii]) * R;                             //  image = tile * opacity 0-100%
         pix3[1] += (0.01 * opacity / tmap[ii]) * G;                             //  reduce for overlapping tiles
         pix3[2] += (0.01 * opacity / tmap[ii]) * B;
      }
   }

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

   for (trow = 0; trow < Ntrows; trow++)                                         //  loop tile rows, columns 
   for (tcol = 0; tcol < Ntcols; tcol++)
   {
      for (tpy = 0; tpy < phh; tpy++)                                            //  loop tile pixels
      for (tpx = 0; tpx < pww; tpx++)
      {
         mpy = trow * (phh-2*olaphh) + tpy;                                      //  corresponding image pixel
         mpx = tcol * (pww-2*olapww) + tpx;
         if (mpy >= Ehh || mpx >= Eww) continue;
         pix3 = PXMpix(E3pxm,mpx,mpy);
         ii = tpy * pww + tpx;
         pbrite = tbmap[ii];                                                     //  tile pixel rel. brightness 0...2
         ii = mpy * Eww + 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 < Ehh; mpy++)                                               //  stop RGB overflows
   for (mpx = 0; mpx < Eww; mpx++)
   {
      pix3 = PXMpix(E3pxm,mpx,mpy);
      RGBfix(pix3[0],pix3[1],pix3[2]);
   }

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

   return;
}


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

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

void m_dither(GtkWidget *, ch *menu)
{
   int   dither_dialog_event(zdialog *zd, ch *event);
   
   zdialog  *zd;
   
   F1_help_topic = "dither";

   Plog(1,"m_dither \n");

   if (! curr_file) {
      zmessageACK(Mwin,"no current file");
      return;
   }

   viewmode('F');
   
/***
          ____________________________________________
         |               Dither Image                 |
         |                                            |
         |  [Dither0]   Roy Lichtenstein effect       |
         |  [Dither1]   pure RGB or black/white dots  |
         |  [Dither2]   RGB mix with given bit-depth  |
         |  [Dither3]   use custom color palette      |
         |  [Dither4]   condense into color blobs     |
         |                                            |
         |                                        [X] |
         |____________________________________________|

***/

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

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

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

   zdialog_add_widget(zd,"hbox","hb3","dialog");
   zdialog_add_widget(zd,"button","dither3","hb3","Dither3","space=3");
   zdialog_add_widget(zd,"label","lab3","hb3","use custom color palette","space=3");

   zdialog_add_widget(zd,"hbox","hb4","dialog");
   zdialog_add_widget(zd,"button","dither4","hb4","Dither4","space=3");
   zdialog_add_widget(zd,"label","lab4","hb4","condense into color blobs","space=3");
   
   zdialog_run(zd,dither_dialog_event,"save");
   return;
}


//  dialog event and completion function

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

   zdialog_free(zd); 
   return 1;
}


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

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

namespace dither0_names
{
   int         dot_size;                                                         //  tile size enclosing dots
   editfunc    EFdither0;
}

void m_dither0(GtkWidget *, ch *menu)
{
   using namespace dither0_names;

   int    dither0_dialog_event(zdialog *zd, ch *event);
   void * dither0_thread(void *);

   F1_help_topic = "dither";

   Plog(1,"m_dither0 \n");

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

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

/**
          __________________________
         |    Roy Lichtenstein      |
         |                          |
         | dot size [ 12 ]          |
         |                          |
         |         [Apply] [OK] [X] |
         |__________________________|
         
**/
   
   zdialog *zd = zdialog_new("Roy Lichtenstein",Mwin,"Apply","OK"," X ",null);
   EFdither0.zd = zd;

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

   dot_size = 9;

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


//  dialog event and completion callback function

int dither0_dialog_event(zdialog * zd, ch *event)
{
   using namespace dither0_names;

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  apply
         zd->zstat = 0;                                                          //  keep dialog active
         zdialog_fetch(zd,"size",dot_size);                                      //  get dot size
         thread_signal();                                                        //  trigger working thread
         return 1;
      }
      else if (zd->zstat == 2) {                                                 //  commit edit
         edit_addhist("dot size:%d",dot_size);                                   //  edit parms > edit hist
         edit_done(0);
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }
   
   return 1;
}


//  image dither0 thread function

void * dither0_thread(void *)
{
   using namespace dither0_names;

   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;
   float       fpi = 1.0 / 3.14159;
   float       dcx, dcy, qcx, qcy, qrad2;
   int         nc = E3pxm->nc;

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

   px1 = 0;                                                                      //  limits for tiles
   px2 = Eww - ds;
   py1 = 0;
   py2 = Ehh - ds;

   if (sa_stat == sa_stat_fini) {                                                //  reduce for active area
      px1 = sa_minx;
      px2 = sa_maxx;
      py1 = sa_miny;
      py2 = sa_maxy;
      if (px2 > Eww - ds) px2 = Eww - ds;
      if (py2 > Ehh - ds) py2 = Ehh - ds;
   }

   shift1 = 0;

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

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

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

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

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

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

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

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

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

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

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

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

      for (ii = 0; ii < Eww * Ehh; ii++)                                         //  loop all pixels
      {
         dist = sa_pixmap[ii];
         if (dist == 0)                                                          //  outside area
            memmove(pix3,pix1,3*sizeof(float));                                  //  restore input pixel
         pix1 += nc;
         pix3 += nc;
      }
   }

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

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


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

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

namespace dither1_names
{
   editfunc    EFdither1;
   int         res, RGB, BW;
   int         rpos;
}


//  menu function

void m_dither1(GtkWidget *, ch *menu)
{
   using namespace dither1_names;

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

   F1_help_topic = "dither";

   Plog(1,"m_dither1 \n");

   EFdither1.menuname = "dither1";
   EFdither1.menufunc = m_dither1;
   EFdither1.threadfunc = dither1_thread;                                        //  thread function
   EFdither1.Farea = 2;                                                          //  select area usable
   EFdither1.Fscript = 1;                                                        //  scripting supported

   if (! edit_setup(EFdither1)) return;                                          //  setup edit
   
/***
       __________________________
      |        Dither1           |
      |                          |
      |  resolution  [ N ]       |          2x2 to 30x30                         //  see Pflag dependency below
      |  (o) RGB color           |
      |  (o) black/white         |
      |  [x] random position     |
      |                          |
      |         [Apply] [OK] [X] |
      |__________________________|
   
***/


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

   zdialog_stuff(zd,"RGB",1);
   zdialog_stuff(zd,"B&W",0);
   zdialog_stuff(zd,"rpos",0);

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

   return;
}


//  dither1 dialog event and completion function

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

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

   zdialog_fetch(zd,"res",res);
   zdialog_fetch(zd,"RGB",RGB);
   zdialog_fetch(zd,"B&W",BW);
   zdialog_fetch(zd,"rpos",rpos);

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  apply
         zd->zstat = 0;
         thread_signal();
      }
      else if (zd->zstat == 2) {                                                 //  done
         edit_addhist("res:%d RGB:%d B/W:%d rpos:%d",res,RGB,BW,rpos);           //  edit parms > edit hist
         edit_done(0);
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   return 1;
}


//  thread function - multiple working threads to update image

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

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

   if (sa_stat == sa_stat_fini) progress_setgoal(sa_Npixel);                     //  initz. progress counter
   else  progress_setgoal(Eww * Ehh);
   
   if (RGB) do_wthreads(dither1_wthread_RGB,NSMP);                               //  do RGB worker threads
   if (BW) do_wthreads(dither1_wthread_BW,NSMP);                                 //  do B/W worker threads
   
   progress_setgoal(0);

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

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


//  worker thread function for RGB dither1

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

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

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

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

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

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

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

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

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

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

            for (jj = 0; jj < res2; jj++)                                        //  loop pixels in block
            {
               if (Pflag[jj]) continue;                                          //  pixel used already
               qy = jj / res;                                                    //  pixel within block
               qx = jj - qy * res;
               pix1 = PXMpix(E1pxm, px + qx, py + qy);                           //  find pixel with greatest
               if (pix1[ii] > maxrgb) {                                          //    matching RGB value
                  maxrgb = pix1[ii]; 
                  kk = jj; 
               }
            }
         
            if (maxrgb > 0)                                                      //  found pixel
            {
               Pflag[kk] = 1;                                                    //  mark pixel as used
               qy = kk / res;                                                    //  matching pixel within block
               qx = kk - res * qy;
               pix3 = PXMpix(E3pxm, px + qx, py + qy);                           //  make into dithered pixel
               pix3[ii] = 255;
            }
         }
      }
      
      progress_addvalue(index,res2);
   }

   return 0;                                                                     //  exit thread
}


//  worker thread function for black/white dither

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

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

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

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

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

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

         minrgb = 9999;

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

            for (jj = 0; jj < res2; jj++)                                        //  loop pixels in block
            {
               kk++;                                                             //  next pixel
               if (kk == res2) kk = 0;
               if (! Pflag[kk]) break;                                           //  available
            }
            
            if (jj < res2)                                                       //  found
            {
               Pflag[kk] = 1;                                                    //  mark pixel as used
               qy = kk / res;                                                    //  pixel within block
               qx = kk - res * qy;
               pix3 = PXMpix(E3pxm, px + qx, py + qy);
               pix3[0] = pix3[1] = pix3[2] = 0;                                  //  make pixel black
            }
         }
         
         else                                                                    //  use best match position in block
         {
            for (jj = 0; jj < res2; jj++)                                        //  loop pixels in block
            {
               if (Pflag[jj]) continue;                                          //  pixel used already
               qy = jj / res;                                                    //  pixel within block
               qx = jj - qy * res;
               pix1 = PXMpix(E1pxm, px + qx, py + qy);
               pixrgb = pix1[0] + pix1[1] + pix1[2];                             //  get pixel RGB sum
               if (pixrgb < minrgb) {
                  minrgb = pixrgb;                                               //  remember pixel with lowest sum
                  kk = jj; 
               }
            }
            
            if (minrgb < 9999)                                                   //  found pixel
            {
               Pflag[kk] = 1;                                                    //  mark pixel as used
               qy = kk / res;                                                    //  matching pixel within block
               qx = kk - res * qy;
               pix3 = PXMpix(E3pxm, px + qx, py + qy);
               pix3[0] = pix3[1] = pix3[2] = 0;                                  //  make pixel black
            }
         }
      }
      
      progress_addvalue(index,res2);
   }

   return 0;                                                                     //  exit thread
}


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

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

namespace dither2_names
{
   editfunc    EFdither2;
   int         res, res2, depth, comp;
}


//  menu function

void m_dither2(GtkWidget *, ch *menu)
{
   using namespace dither2_names;

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

   F1_help_topic = "dither";

   Plog(1,"m_dither2 \n");

   EFdither2.menuname = "dither2";
   EFdither2.menufunc = m_dither2;
   EFdither2.threadfunc = dither2_thread;                                        //  thread function
   EFdither2.Farea = 2;                                                          //  select area usable
   EFdither2.Fscript = 1;                                                        //  scripting supported

   if (! edit_setup(EFdither2)) return;                                          //  setup edit
   
/***
       __________________________
      |        Dither2           |
      |                          |
      |  resolution  [ N ]       |          2x2 to 30x30
      |  color depth [ N ]       |          1 to 8 bits
      |  [x] error compensation  |
      |                          |
      |         [Apply] [OK] [X] |
      |__________________________|
   
***/

   zdialog *zd = zdialog_new("Dither2",Mwin,"Apply","OK"," X ",null);            //  dither dialog
   CEF->zd = zd;
   
   zdialog_add_widget(zd,"hbox","hbres","dialog");
   zdialog_add_widget(zd,"label","labres","hbres","resolution","space=5");
   zdialog_add_widget(zd,"zspin","res","hbres","2|30|1|4","space=5|size=2");
   zdialog_add_widget(zd,"hbox","hbdep","dialog");
   zdialog_add_widget(zd,"label","labdep","hbdep","color depth","space=5");
   zdialog_add_widget(zd,"zspin","depth","hbdep","1|8|1|2","space=5|size=2");
   zdialog_add_widget(zd,"hbox","hbcomp","dialog");
   zdialog_add_widget(zd,"check","comp","hbcomp","error compensation","space=5"); 

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

   return;
}


//  dither2 dialog event and completion function

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

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

   zdialog_fetch(zd,"res",res);
   zdialog_fetch(zd,"depth",depth);
   zdialog_fetch(zd,"comp",comp);

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  apply
         zd->zstat = 0;
         thread_signal();
      }
      else if (zd->zstat == 2) {                                                 //  done
         edit_addhist("res:%d depth:%d comp:%d",res,depth,comp);                 //  edit parms > edit hist
         edit_done(0);
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   return 1;
}


//  thread function

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

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

   bb = bits[9-depth];                                                           //  depth: 1 .. 8   bb: 256 128 64 .. 2
   res2 = res * res;                                                             //  pixel block size
   Bcols = Eww / res;                                                            //  block rows and cols
   Brows = Ehh / res;

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

   progress_setgoal(Nblocks);                                                    //  initz. progress counter

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

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

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

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

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

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

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

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

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

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

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

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

      progress_addvalue(1);
   }

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

   progress_setgoal(0);

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


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

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

namespace dither3_names
{
   editfunc    EFdither3;
   int         res, res2, comp;
   ch          palettename[100] = "none";                                        //  palette file name
   int         Ncolors = 0;                                                      //  palette color count
}


//  menu function

void m_dither3(GtkWidget *, ch *menu)
{
   using namespace dither3_names;

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

   F1_help_topic = "dither";

   Plog(1,"m_dither3 \n");

   EFdither3.menuname = "dither3";
   EFdither3.menufunc = m_dither3;
   EFdither3.threadfunc = dither3_thread;                                        //  thread function
   EFdither3.Farea = 2;                                                          //  select area usable
   EFdither3.Fscript = 1;                                                        //  script file supported

   if (! edit_setup(EFdither3)) return;                                          //  setup edit
   
   if (Fscriptrun) Ncolors = 0;

/***
       _________________________________
      |           Dither3               |
      |                                 |
      |  resolution  [ N ]              |                                        //  2x2 to 30x30
      |  palette: [palette name] [load] |
      |  [x] error compensation         |
      |                                 |
      |                [Apply] [OK] [X] |
      |_________________________________|
   
***/

   zdialog *zd = zdialog_new("Dither3",Mwin,"Apply","OK"," X ",null);            //  dither dialog
   CEF->zd = zd;
   
   zdialog_add_widget(zd,"hbox","hbres","dialog");
   zdialog_add_widget(zd,"label","labres","hbres","resolution","space=5");
   zdialog_add_widget(zd,"zspin","res","hbres","2|30|1|4","space=5|size=2");
   zdialog_add_widget(zd,"hbox","hbpal","dialog");
   zdialog_add_widget(zd,"hbox","hbpal","dialog");
   zdialog_add_widget(zd,"label","labpal","hbpal","palette:","space=5");
   zdialog_add_widget(zd,"zentry","palname","hbpal","none","size=20|space=5");
   zdialog_add_widget(zd,"button","LOAD","hbpal","Load","space=5");
   zdialog_add_widget(zd,"hbox","hbcomp","dialog");
   zdialog_add_widget(zd,"check","comp","hbcomp","error compensation","space=5");

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

   res = 4;
   comp = 0;
   return;
}


//  dither3 dialog event and completion function

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

   int load_palette_map(ch *file);

   ch    palettefile[200], *pp;

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

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

         zdialog_fetch(zd,"res",res);
         zdialog_fetch(zd,"comp",comp);
         zdialog_fetch(zd,"palname",palettename,100);

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

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

      else if (zd->zstat == 2) {                                                 //  [OK]
         edit_addhist("res:%d palette:%s comp:%d",res,palettename,comp);         //  edit parms > edit hist
         edit_done(0);
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

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


//  thread function

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

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

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

   res2 = res * res;                                                             //  pixel block size
   Bcols = Eww / res;                                                            //  block rows and cols
   Brows = Ehh / res;

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

   progress_setgoal(Nblocks);                                                    //  initz. progress counter

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

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

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

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

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

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

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

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

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

      progress_addvalue(1);
   }

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

   progress_setgoal(0);

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


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

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


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

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

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

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

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

   return Np;
}


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

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

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

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

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

   return 0;
}


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

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

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


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

//  The pixels in an image attract each-other and collapse into 
//    blobs at a local center of mass (mass := brightness). 

namespace dither4_names
{
   editfunc    EFdither4;
   int         radius;
   float       *vposx, *vposy;                                                   //  pixel virtual positions
   float       *wposx, *wposy;
   int         Frun;                                                             //  run blob condensation process
}


//  menu function

void m_dither4(GtkWidget *, ch *menu)
{
   using namespace dither4_names;

   int    dither4_dialog_event(zdialog* zd, ch  *event);
   void * dither4_thread(void *);

   int      px, py, ii;
   uint     vpcc;

   F1_help_topic = "dither";

   Plog(1,"m_dither4 \n");

   EFdither4.menuname = "dither4";
   EFdither4.menufunc = m_dither4;
   EFdither4.threadfunc = dither4_thread;
   EFdither4.Farea = 2;                                                          //  select area usable

   if (! edit_setup(EFdither4)) return;                                          //  setup edit
   
   vpcc = Eww * Ehh * sizeof(float);
   vposx = (float *) zmalloc(vpcc,"dither4");                                    //  virtual pixel positions
   vposy = (float *) zmalloc(vpcc,"dither4");
   wposx = (float *) zmalloc(vpcc,"dither4");
   wposy = (float *) zmalloc(vpcc,"dither4");

   for (py = 0; py < Ehh; py++)                                                  //  initz. virtual pixel positions
   for (px = 0; px < Eww; px++) 
   {
      ii = py * Eww + px;
      vposx[ii] = drandz() + px;
      vposy[ii] = drandz() + py;
   }

/***
       ________________________________
      |           Dither4              |
      |                                |
      | radius [ 10 ]  [start] [stop]  |
      |                                |
      |               [Reset] [OK] [X] |  
      |________________________________|

***/

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

   zdialog_add_widget(zd,"hbox","hbinputs","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labradius","hbinputs","radius","space=3");
   zdialog_add_widget(zd,"zspin","radius","hbinputs","2|20|1|10","space=3");
   zdialog_add_widget(zd,"button","start","hbinputs","start","space=5");
   zdialog_add_widget(zd,"button","stop","hbinputs","stop","space=5");
   zdialog_add_ttip(zd,"start","start blob condensation process"); 
   zdialog_add_ttip(zd,"stop","stop blob condensation process"); 

   radius = 10;                                                                   //  initial value

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


//  dialog event and completion callback function

int dither4_dialog_event(zdialog *zd, ch  *event)
{
   using namespace dither4_names;
   
   int  dither4_timerfunc(void *);

   int   px, py, ii;
   
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  apply and quit
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  cancel
   
   if (zd->zstat) Frun = 0;                                                      //  stop thread

   if (zd->zstat == 1)                                                           //  [reset]
   {
      zd->zstat = 0;                                                             //  keep dialog active
      edit_reset();
      
      for (py = 0; py < Ehh; py++)                                               //  reset virtual pixel positions
      for (px = 0; px < Eww; px++) 
      {
         ii = py * Eww + px;
         vposx[ii] = drandz() + px;
         vposy[ii] = drandz() + py;
      }

      return 1;
   }

   if (zd->zstat == 2)                                                           //  [OK]
   {
      if (CEF->Fmods) {
         edit_addhist("radius:%d",radius);                                       //  edit parms > edit hist
         edit_done(0);                                                           //  complete edit
      }
      else edit_cancel(0);                                                       //  no change

      zfree(vposx);                                                              //  free memory
      zfree(vposy);
      zfree(wposx);
      zfree(wposy);
      return 1;
   }
   
   if (zd->zstat < 0 || zd->zstat > 2) {                                         //  cancel edit
      edit_cancel(0);
      zfree(vposx);                                                              //  free memory
      zfree(vposy);
      zfree(wposx);
      zfree(wposy);
      return 1;
   }
   
   if (strmatch(event,"radius")) zdialog_fetch(zd,"radius",radius);

   if (strmatch(event,"start")) 
   {
      if (! Frun) {
         Frun = 1;                                                               //  start timer function
         g_timeout_add(200,dither4_timerfunc,0);                                 //  runs every 200 ms                     24.50
      }
   }

   else Frun = 0;
   
   return 1;
}


//  timer function - start thread every interval

int dither4_timerfunc(void *)                                                    //  24.50
{
   using namespace dither4_names;

   if (! Frun) return 0;                                                         //  exit forever

   thread_signal();                                                              //  start thread process
   thread_wait();

   if (CEF) {
      CEF->Fmods = 1;                                                            //  image E3 modified
      CEF->Fsaved = 0;
   }
   
   Fpaint2();                                                                    //  update window

   return 1;                                                                     //  come back every interval
}


//  thread function

void * dither4_thread(void *arg)
{
   using namespace dither4_names;

   void * dither4_wthread(void *);
   
   synch_threads(NSMP);                                                          //  setup to synch threads
   do_wthreads(dither4_wthread,NSMP);                                            //  do working threads

   return 0;
}


//  worker thread function

void * dither4_wthread(void *arg)
{
   using namespace dither4_names;

   int      index = *((int *) arg);
   int      px, py, qx, qy;
   int      ii, jj, rgb, dist;
   float    vpx, vpy, vqx, vqy, dx, dy;
   float    R, R2, M;
   float    Fx, Fy, Fxa, Fya;
   float    *pix1, *pix3, *pix3q;
   float    c1, c2;
   
   for (py = radius+index; py < Ehh-radius; py += NSMP)                          //  loop pixels px/py in input image
   for (px = radius; px < Eww-radius; px++)
   {
      ii = py * Eww + px;

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

      wposx[ii] = vposx[ii];                                                     //  copy px/py virtual position
      wposy[ii] = vposy[ii];
   }

   for (py = radius+index; py < Ehh-radius; py += NSMP)                          //  loop pixels px/py in input image
   for (px = radius; px < Eww-radius; px++)
   {
      ii = py * Eww + px;

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

      vpx = vposx[ii];                                                           //  current px/py virtual positions
      vpy = vposy[ii];
      
      Fx = Fy = 0;                                                               //  force acting on px/py
      
      for (qy = py-radius; qy <= py+radius; qy++)                                //  loop pixels qx/qy in range of px/py
      for (qx = px-radius; qx <= px+radius; qx++)
      {
         jj = qy * Eww + qx;
         vqx = vposx[jj];                                                        //  current qx/qy virtual positions
         vqy = vposy[jj];
         
         dx = vqx - vpx;                                                         //  vpx/vpy -> vqx/vqy
         dy = vqy - vpy;                                                         //    x and y distance
         
         R2 = dx * dx + dy * dy;                                                 //  distance ^2
         if (R2 > radius * radius) continue;                                     //  vqx/vqy out of range, ignore
         if (R2 < 0.5) continue;                                                 //  too close, ignore
         
         pix1 = PXMpix(E1pxm,qx,qy);                                             //  get pixel qx/qy RGB

         M = pix1[0] + pix1[1] + pix1[2]; 
         M = 0.001 * M / R2;                                                     //  'mass' of pixel, 0 - 0.77
         Fx += M * dx;                                                           //  accumulate total attractive force
         Fy += M * dy;                                                           //    of all pixels in range
      }
      
      Fxa = fabsf(Fx);                                                           //  get |Fx| and |Fy|
      Fya = fabsf(Fy);
      
      if (Fxa + Fya < 0.1) continue;                                             //  too small, ignore
      
      if (Fxa > Fya) {                                                           //  scale Fx/Fy to 1.0
         Fy = Fy / Fxa;
         Fx = Fx / Fxa;
      }
      else {
         Fx = Fx / Fya;
         Fy = Fy / Fya;
      }
      
      wposx[ii] += 0.7 * Fx;                                                     //  move virt. px/py in force direction
      wposy[ii] += 0.7 * Fy;
   }
   
   for (py = radius+index; py < Ehh-radius; py += NSMP)                          //  loop pixels px/py in input image
   for (px = radius; px < Eww-radius; px++)
   {
      ii = py * Eww + px;

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

      vposx[ii] = wposx[ii];                                                     //  get new px/py virtual position
      vposy[ii] = wposy[ii];
   }

   synch_threads(0);                                                             //  wait for all threads reach here
   
   if (index > 0) return 0;                                                      //  continue only thread 0

   for (py = 0; py < Ehh; py++)                                                  //  loop pixels px/py in output image
   for (px = 0; px < Eww; px++)
   {
      ii = py * Eww + px;

      if (sa_stat == sa_stat_fini) {                                             //  select area active
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel outside area
      }
      
      pix3 = PXMpix(E3pxm,px,py);                                                //  set pixel RGB values = 0
      pix3[0] = pix3[1] = pix3[2] = 0;
   }

   for (py = radius; py < Ehh-radius; py++)                                      //  loop pixels px/py in output image
   for (px = radius; px < Eww-radius; px++)
   {
      ii = py * Eww + px;

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

      qx = vposx[ii] + 0.5;                                                      //  new pixel virtual position
      qy = vposy[ii] + 0.5;
      if (qx < 0 || qx > Eww-1) continue;
      if (qy < 0 || qy > Ehh-1) continue;

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

      pix3 = PXMpix(E3pxm,qx,qy);                                                //  move RGB values to new position
      pix3[0] += pix1[0];
      pix3[1] += pix1[1];
      pix3[2] += pix1[2];
   }

   for (py = radius; py < Ehh-radius; py++)                                      //  loop pixels px/py in output image
   for (px = radius; px < Eww-radius; px++)
   {
      if (sa_stat == sa_stat_fini) {                                             //  select area active
         ii = py * Eww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel outside area
      }

      pix3 = PXMpix(E3pxm,px,py);
      
      for (rgb = 0; rgb < 3; rgb++)                                              //  loop RGB color
      {
         c1 = pix3[rgb];                                                         //  pixel RGB value
         if (c1 <= 255) continue;                                                //    <= 255, OK
         pix3[rgb] = 255;                                                        //     > 255, must distribute excess
         c1 = c1 - 255;                                                          //  amount to distribute

         for (R = 1; R <= radius; R++)                                           //  loop pixels qx/qy in range of px/py
         {
            for (qy = -R; qy <= +R; qy++)
            for (qx = -R; qx <= +R; qx++)
            {
               if (qx == 0 && qy == 0) continue;                                 //  skip px/py
               if (sqrtf(qx*qx + qy*qy) > R) continue;                           //  corners of box > range
               
               pix3q = PXMpix(E3pxm,px+qx,py+qy);
               c2 = pix3q[rgb];

               if (c2 >= 255) continue;                                          //  pixel already full

               if (c1 + c2 > 255) {
                  c1 -= (255 - c2);                                              //  fill pixel and reduce
                  c2 = 255;                                                      //    remaining amount to distribute
               }
               else {
                  c2 += c1;                                                      //  add amount to distribute to pixel
                  c1 = 0;                                                        //  nothing left
               }
               pix3q[rgb] = c2;

               if (c1 < 1) goto endR;
            }
         }
         endR:;
      }
   }

   return 0;
}


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

//  Convert an image into a form of line art.
//  Replace RGB pixel blocks with horizontal stripes of B/W pixels.

namespace engrave_names
{
   editfunc    EFengrave;
   int         size, bias, color;                                                //  user inputs
}


//  menu function

void m_engrave(GtkWidget *, ch *menu)
{
   using namespace engrave_names;

   int    engrave_dialog_event(zdialog* zd, ch  *event);
   void   engrave_curvedit(int spc);
   void * engrave_thread(void *);

   F1_help_topic = "engrave";

   Plog(1,"m_engrave \n");

   EFengrave.menuname = "Engrave";
   EFengrave.menufunc = m_engrave;
   EFengrave.Farea = 2;                                                          //  select area usable
   EFengrave.Fscript = 1;                                                        //  scripting supported
   EFengrave.threadfunc = engrave_thread;

   if (! edit_setup(EFengrave)) return;                                          //  setup edit
   
/***
             ______________________________________
            |               Engrave                |
            |                                      |
            | Size [___]  Bias [___]  color [___]  |
            |                                      |
            |                     [Apply] [OK] [X] |
            |______________________________________|

***/

   zdialog *zd = zdialog_new("Engrave",Mwin,"Apply","OK"," X ",null);
   EFengrave.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labsize","hb1","Size","space=3");
   zdialog_add_widget(zd,"zspin","size","hb1","2|20|1|8");
   zdialog_add_widget(zd,"label","space","hb1",0,"space=4");
   zdialog_add_widget(zd,"label","labbias","hb1","Bias","space=3");
   zdialog_add_widget(zd,"zspin","bias","hb1","-255|255|1|0");
   zdialog_add_widget(zd,"label","space","hb1",0,"space=4");
   zdialog_add_widget(zd,"label","labcolor","hb1","Color","space=3");
   zdialog_add_widget(zd,"zspin","color","hb1","0|100|1|0");

   size = 8;                                                                     //  initial values
   bias = 0;
   color = 0;

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

   return;
}


//  dialog event and completion callback function

int engrave_dialog_event(zdialog *zd, ch  *event)
{
   using namespace engrave_names;

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

   if (zd->zstat == 1)                                                           //  [apply]
   {
      zd->zstat = 0;                                                             //  keep dialog active
      zdialog_fetch(zd,"size",size);                                             //  get user inputs
      zdialog_fetch(zd,"bias",bias);
      zdialog_fetch(zd,"color",color);
      thread_signal();                                                           //  do function
      return 1;
   }

   if (zd->zstat == 2)                                                           //  [OK]
   {
      if (CEF->Fmods) edit_done(0);
      else edit_cancel(0);
      return 1;
   }
   
   if (zd->zstat < 0 || zd->zstat > 2) {                                         //  cancel edit
      edit_cancel(0);
      return 1;
   }
   
   return 1;
}


//  thread function

void * engrave_thread(void *arg)
{
   using namespace engrave_names;

   void * engrave_wthread(void *);
   
   do_wthreads(engrave_wthread,NSMP);

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

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


void * engrave_wthread(void *arg)                                                //  worker thread function
{
   using namespace engrave_names;

   int         index = *((int *) arg);
   int         ii, px, py, qx, qy, dist;
   int         pcc = E3pxm->nc * sizeof(float);
   float       brite, dnew, dold;
   float       *pix1, *pix3;
   
   for (py = index * size; py < Ehh-size; py += NSMP * size)                     //  loop pixel blocks size x size
   for (px = 0; px < Eww-size; px += size)
   {
      for (qx = px; qx < px + size; qx++)                                        //  loop block columns
      {
         brite = bias * size;                                                    //  initial bias

         for (qy = py; qy < py + size; qy++)                                     //  loop pixels in column
         {
            pix1 = PXMpix(E1pxm,qx,qy);                                          //  input pixel
            brite += PIXBRIGHT(pix1);                                            //  sum column brightness
         }
         
         for (qy = py; qy < py + size; qy++)                                     //  loop pixels in column
         {
            pix3 = PXMpix(E3pxm,qx,qy);                                          //  output pixel
            if (brite < 100) pix3[0] = pix3[1] = pix3[2] = 0;                    //  set column pixels to black or white
            else pix3[0] = pix3[1] = pix3[2] = 255;                              //  B/W ratio matches block brightness
            brite -= 255;
         }

         if (color > 1) 
         {
            dnew = 0.01 * color;                                                 //  add color 0 ... 1
            dold = 1.0 - dnew;
            
            for (qy = py; qy < py + size; qy++)                                  //  loop pixels in column
            {
               pix3 = PXMpix(E3pxm,qx,qy);                                       //  output pixel
               for (ii = 0; ii < 3; ii++)
                  if (pix3[ii] == 255) 
                     pix3[ii] = dnew * pix1[ii] + dold * 255;
            }
         }

         if (sa_stat == sa_stat_fini)                                            //  select area active
         {
            for (qy = py; qy < py + size; qy++)                                  //  loop pixels in column
            {
               pix1 = PXMpix(E1pxm,qx,qy);
               pix3 = PXMpix(E3pxm,qx,qy);
               ii = qy * Eww + qx;
               dist = sa_pixmap[ii];
               if (! dist) {                                                     //  pixel outside area
                  memcpy(pix3,pix1,pcc);                                         //  restore input pixel
                  continue;
               }
            }
         }
      }
   }

   return 0;
}


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

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

namespace mosaic_names
{
   editfunc EFmosaic;                        //  edit function data

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

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

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


//  menu function

void m_mosaic(GtkWidget *, ch *menu)
{
   using namespace mosaic_names;

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

   zdialog  *zd;
   ch       *title = "Create Mosaic";
   ch       label[12];
   int      cc;

   F1_help_topic = "mosaic";

   Plog(1,"m_mosaic \n");

   EFmosaic.menuname = "Mosaic";
   EFmosaic.mousefunc = mosaic_mousefunc;                                        //  mouse function
   EFmosaic.Farea = 2;                                                           //  area usable

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

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

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

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

   cc = Eww * Ehh * sizeof(int);                                                 //  allocate image/tile map
   tilemap = (int *) zmalloc(cc,"mosaic");
   memset(tilemap,-1,cc);

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

***/

   zd = zdialog_new(title,Mwin,"OK"," X ",null);                                 //  mosaic dialog
   CEF->zd = zd;

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

   zdialog_add_widget(zd,"hbox","hbcreate","dialog");
   zdialog_add_widget(zd,"button","tiles","hbcreate","Tiles","space=3");
   zdialog_add_widget(zd,"label","labNtiles","hbcreate","0","space=5");
   zdialog_add_widget(zd,"button","image","hbcreate","Image","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","Tile blending","space=3");
   zdialog_add_widget(zd,"hscale","blend","hbblend","0|100|1|0","space=5|expand");

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

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

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


//  mosaic dialog event and completion function

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

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

   if (block) return 1;
   block = 1;   

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

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

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

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

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

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

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

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

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

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

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

      zfree(flist);

      Ntiles = NF;

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

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

      block = 0;
      return 1;
   }

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

      edit_undo();                                                               //  reset to original image

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

      for (int ii = 0; ii < NSMP; ii++) {                                        //  wait for completion
         while(tbusy[ii]) {
            zmainsleep(0.01);
            Fpaint2();                                                           //  update image continuously
         }
      }
      
      zdialog_stuff(zd,"blend",0);                                               //  reset blend control
      
      if (sa_stat == sa_stat_fini) sa_postfix();                                 //  if select area, restore unselected

      Fpaint2();

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

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

      for (trow = 0; trow < Ehh/thh; trow++)                                     //  loop tile positions in base image
      for (tcol = 0; tcol < Eww/tww; tcol++)
      {
         for (tpy = 0; tpy < thh; tpy++)                                         //  loop tile pixels
         for (tpx = 0; tpx < tww; tpx++)
         {
            ipy = trow * thh + tpy;                                              //  corresponding image pixels
            ipx = tcol * tww + tpx;
            ii = Eww * ipy + ipx;                                                //  get tile used at image pixel
            ii = tilemap[ii];
            if (ii < 0 || ii >= maxtiles) break;
            timage = tileimage[ii];
            if (! timage) continue;
            tpix = timage + tpy * tww * 3 + tpx * 3;                             //  input tile pixel
            pix1 = PXMpix(E1pxm,ipx,ipy);                                        //  input E1 image pixel
            pix3 = PXMpix(E3pxm,ipx,ipy);                                        //  output E3 image pixel
            pix3[0] = f1 * pix1[0] + f3 * tpix[0];                               //  replace image pixels with tile pixels
            pix3[1] = f1 * pix1[1] + f3 * tpix[1];
            pix3[2] = f1 * pix1[2] + f3 * tpix[2];
         }
      }
      
      if (sa_stat == sa_stat_fini) sa_postfix();                                 //  if select area, restore unselected

      Fpaint2();

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

   block = 0;
   return 1;
}


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

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

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

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

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

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

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

      if (tww1 * thh < tww * thh1) {                                             //  tww1/thh1 < tww/thh
         tww2 = tww;                                                             //    (too high for tww x thh)
         thh2 = tww * thh1 / tww1;
      }
      else {                                                                     //  tww1/thh1 > tww/thh
         thh2 = thh;                                                             //    (too wide for tww x thh)
         tww2 = thh * tww1 / thh1;
      }
      
      pxb2 = gdk_pixbuf_new_from_file_at_size(tilefile[ii],tww2+1,thh2,&gerror); //  tww2+1 required (see below)
      if (! pxb2) {
         Plog(1,"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,"mosaic");                        //  allocate memory for tww x thh pixels
      
      for (row = 0; row < thh; row++)
      {                                                                          //  copy pixbuf tww x thh section
         row1 = tpixels + rs * (row + tpy) + tpx * 3;
         row2 = timage + row * tww * 3;
         memcpy(row2,row1,tww * 3);                                              //  buffer-overflow unless (see above)
      }

      g_object_unref(pxb2);

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

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

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

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

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

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

      coeff = 0.25 / (tww * thh);

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

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

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

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


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

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

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

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

   float    match, bestmatch;
   float    coeff;

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

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

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

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

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

      coeff = 0.25 / (tww * thh);

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

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

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

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

      bestmatch = 0;
      bestii = 0;

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

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

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

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

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

   tbusy[index] = 0;
   return 0;
}


//  mouse function - called for mouse events inside image

void mosaic_mousefunc()
{
   using namespace mosaic_names;

   int      ii;
   ch       *tfile, *mfile;

   if (! LMclick) return;
   LMclick = 0;

   ii = Myclick * Eww + 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;
}


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

//  Add random noise to an image (can mask color banding)

namespace  add_noise_names
{
   editfunc    EFadd_noise;
   float       size;                      //  mean noise size, pixels (diameter)
   float       level;                     //  mean noise level, 0-100% 
   float       Pmap[22][22];              //  pixel noise distribution map (max. size 20)
}


//  menu function

void m_add_noise(GtkWidget *, ch *menu)
{
   using namespace add_noise_names;

   int    add_noise_dialog_event(zdialog* zd, ch *event);
   void * add_noise_thread(void *);

   F1_help_topic = "add noise";

   Plog(1,"m_add_noise \n");

   EFadd_noise.menufunc = m_add_noise;
   EFadd_noise.menuname = "Add Noise";
   EFadd_noise.Farea = 2;                                                        //  select area usable
   EFadd_noise.threadfunc = add_noise_thread;                                    //  thread function

   if (! edit_setup(EFadd_noise)) return;                                        //  setup edit
   
/***
          ___________________________________
         |        Add Random Noise           |
         |                                   |
         | Noise Size [___]  Level [___]     |
         |                                   |
         |          [Reset] [Apply] [OK] [X] |
         |___________________________________|
         
***/

   zdialog *zd = zdialog_new("Add Random Noise",Mwin,"Reset","Apply","OK"," X ",null);
   CEF->zd = zd;
   
   zdialog_add_widget(zd,"hbox","hbnoise","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labsize","hbnoise","Noise Size","space=5");
   zdialog_add_widget(zd,"zspin","size","hbnoise","1|20|1|2");
   zdialog_add_widget(zd,"label","space","hbnoise",0,"space=8");
   zdialog_add_widget(zd,"label","lablevel","hbnoise","Level","space=5");
   zdialog_add_widget(zd,"zspin","level","hbnoise","0|100|1|10");

   size = 2;
   level = 10;
   
   zdialog_resize(zd,300,0);
   zdialog_run(zd,add_noise_dialog_event,"save");                                //  run dialog - parallel

   return;
}


//  dialog event and completion function

int add_noise_dialog_event(zdialog *zd, ch *event)
{
   using namespace add_noise_names;
   
   if (strmatch(event,"done")) zd->zstat = 3;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 4;                                  //  from f_open()
   
   int      rx, ry, irad;
   float    r, frad;
   float    F, sumF;

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

      else if (zd->zstat == 2) {                                                 //  [apply]
         thread_wait();
         zd->zstat = 0;                                                          //  keep dialog active
         PXM_free(E3pxm);
         E3pxm = PXM_copy(E1pxm);
         thread_signal();                                                        //  perform the edit
      }

      else if (zd->zstat == 3)                                                   //  [OK] commit edit
      {
         edit_addhist("size:%.0f level:%.0f",size,level);                        //  edit parms > edit hist
         edit_done(0);
      }

      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   if (strstr("size level",event)) 
   {
      zdialog_fetch(zd,"size",size);
      zdialog_fetch(zd,"level",level);
      
      frad = 0.5 + 0.5 * size;                                                   //  frad:  1.0  1.5  2.0  2.5 ... 10.5
      irad = int(frad);                                                          //  irad:  1    1    2    2   ... 10

      sumF = 0;

      for (ry = -irad; ry <= +irad; ry++)                                        //  compute distribution factors for
      for (rx = -irad; rx <= +irad; rx++)                                        //    noise centered at [irad][irad]
      {
         r = sqrtf(ry*ry + rx*rx);
         if (r >= frad) F = 0;
         else F = sqrtf(1.0 - r/frad); 
         Pmap[rx+irad][ry+irad] = F;
         sumF += F;
      }

      for (ry = -irad; ry <= +irad; ry++)                                        //  normalize to sum = 1.0
      for (rx = -irad; rx <= +irad; rx++)
         Pmap[rx+irad][ry+irad] /= sqrtf(sumF);
   }
   
   return 1;
}


//  thread function - multiple working threads to update image

void * add_noise_thread(void *)
{
   using namespace add_noise_names;

   void  * add_noise_wthread(void *arg);                                         //  worker thread
   
   if (sa_stat == sa_stat_fini) progress_setgoal(sa_Npixel);                     //  set up progress monitor
   else  progress_setgoal(Eww * Ehh);
   
   do_wthreads(add_noise_wthread,NSMP);                                          //  do worker threads
   
   progress_setgoal(0);

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

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


//  worker thread function

void * add_noise_wthread(void *arg)
{
   using namespace add_noise_names;

   int      index = *((int *) arg);
   int      px, py, ii, dist = 1;
   float    *pix3;
   int      rx, ry, irad;
   float    frad, F;
   float    Rn1, Gn1, Bn1, Rn2, Gn2, Bn2;
   
   frad = 0.5 + 0.5 * size;                                                      //  frad:  1.0  1.5  2.0  2.5 ... 10.5
   irad = int(frad);                                                             //  irad:  1    1    2    2   ... 10

   for (py = index+irad; py < Ehh-irad; py += NSMP)                              //  loop all image pixels
   for (px = irad; px < Eww-irad; px++)
   {
      if (sa_stat == sa_stat_fini) {                                             //  select area active
         ii = py * Eww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }
      
      Rn1 = 0.01 * level * (-1.0 + 2 * drandz()) * 127;                          //  RGB noise levels for pixel
      Gn1 = 0.01 * level * (-1.0 + 2 * drandz()) * 127;
      Bn1 = 0.01 * level * (-1.0 + 2 * drandz()) * 127;
      
      for (ry = -irad; ry <= +irad; ry++)                                        //  noise size around pixel
      for (rx = -irad; rx <= +irad; rx++)
      {
         F = Pmap[rx+irad][ry+irad];

         Rn2 = Rn1 * F;                                                          //  spread noise around pixel
         Gn2 = Gn1 * F;
         Bn2 = Bn1 * F;

         pix3 = PXMpix(E3pxm,px+rx,py+ry);                                       //  input/output pixel

         pix3[0] += Rn2;                                                         //  add noise
         pix3[1] += Gn2; 
         pix3[2] += Bn2; 

         RGBfix(pix3[0],pix3[1],pix3[2]);                                        //  stop underflow and overflow           24.50
      }

      progress_addvalue(index,1);                                                //  count progress
   }

   return 0;                                                                     //  exit thread
}


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

//  image set colors function
//  set no. of colors for R/G/B and/or total colors

namespace set_colors_names
{
   int         Rcolors, Gcolors, Bcolors;                                        //  number of RGB colors, 0-256
   int         Tcolors;                                                          //  total colors, R x G x B 
   editfunc    EFsetcolors;
}


//  menu function

void m_set_colors(GtkWidget *, ch *menu)                                         //  24.30
{
   using namespace set_colors_names;

   int    set_colors_dialog_event(zdialog *zd, ch *event);
   void * set_colors_thread(void *);

   F1_help_topic = "set colors";

   Plog(1,"m_set_colors \n");

   EFsetcolors.menuname = "Set Colors";
   EFsetcolors.menufunc = m_set_colors;
   EFsetcolors.FprevReq = 1;                                                     //  use preview
   EFsetcolors.Farea = 2;                                                        //  select area usable
   EFsetcolors.Fpaintedits = 1;                                                  //  can use with paint edits
   EFsetcolors.Fscript = 1;                                                      //  scripting supported
   EFsetcolors.threadfunc = set_colors_thread;                                   //  thread function

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

/***
          _________________________________
         |     Set No. of Colors           |
         |                                 |
         | Red [__]  Green [__]  Blue [__] |
         | Total Colors [_____]            |
         |                                 |
         |                        [OK] [X] |
         |_________________________________|

***/

   zdialog *zd = zdialog_new("Set No. of Colors",Mwin,"OK"," X ",null);
   EFsetcolors.zd = zd;
   zdialog_add_widget(zd,"hbox","hbrgb","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","Rlab","hbrgb","   Red","space=3");
   zdialog_add_widget(zd,"zspin","Rcolors","hbrgb","1|256|1|6","space=3");
   zdialog_add_widget(zd,"label","Glab","hbrgb","   Green","space=3");
   zdialog_add_widget(zd,"zspin","Gcolors","hbrgb","1|256|1|6","space=3");
   zdialog_add_widget(zd,"label","Blab","hbrgb","   Blue","space=3");
   zdialog_add_widget(zd,"zspin","Bcolors","hbrgb","1|256|1|6","space=3");
   zdialog_add_widget(zd,"hbox","hbtot","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","Tlab","hbtot","   Total Colors","space=3");
   zdialog_add_widget(zd,"zspin","Tcolors","hbtot","1|1000000|1|216","space=3");
   
   Rcolors = Gcolors = Bcolors = 6;                                              //  initial no. colors
   Tcolors = 216;                                                                //  6 * 6 * 6 

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

   thread_signal();
   return;
}


//  set_colors dialog event and completion function

int set_colors_dialog_event(zdialog *zd, ch *event)
{
   using namespace set_colors_names;
   
   int      Tcolors2;

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

   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();
      Eww = E3pxm->ww;
      Ehh = E3pxm->hh;
      thread_signal();
      thread_wait();                                                             //  required for paint edits
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  done
         edit_fullsize();                                                        //  get full size image
         Eww = E3pxm->ww;
         Ehh = E3pxm->hh;
         thread_signal();
         edit_addhist("set colors:%d/%d/%d",Rcolors,Gcolors,Bcolors);
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

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

   if (strstr("Rcolors Gcolors Bcolors",event))                                  //  RGB colors changed
   {
      zdialog_fetch(zd,"Rcolors",Rcolors);                                       //  get new RGB colors
      zdialog_fetch(zd,"Gcolors",Gcolors);
      zdialog_fetch(zd,"Bcolors",Bcolors);

      Tcolors = (Rcolors * Gcolors * Bcolors);                                   //  new total colors
      zdialog_stuff(zd,"Tcolors",Tcolors);

      thread_signal();
   }
   
   if (strmatch(event,"Tcolors"))                                                //  total colors changed
   {
      zdialog_fetch(zd,"Tcolors",Tcolors2);                                      //  new total colors

      if (Tcolors2 > Tcolors) {
         Rcolors++;                                                              //  increase RGB colors
         Gcolors++;
         Bcolors++;
         if (Rcolors > 256) Rcolors = 256;
         if (Gcolors > 256) Gcolors = 256;
         if (Bcolors > 256) Bcolors = 256;
      }
      else {
         Rcolors--;                                                              //  decrease RGB colors
         Gcolors--;
         Bcolors--;
         if (Rcolors < 1) Rcolors = 1;
         if (Gcolors < 1) Gcolors = 1;
         if (Bcolors < 1) Bcolors = 1;
      }
      
      Tcolors = (Rcolors * Gcolors * Bcolors);                                   //  new total colors

      zdialog_stuff(zd,"Rcolors",Rcolors);                                       //  update dialog
      zdialog_stuff(zd,"Gcolors",Gcolors);
      zdialog_stuff(zd,"Bcolors",Bcolors);
      zdialog_stuff(zd,"Tcolors",Tcolors);

      thread_signal();
   }

   return 0;
}


//  set_colors thread function

void * set_colors_thread(void *)
{
   using namespace set_colors_names;

   int         px, py, Fend;
   float       *pix1, *pix3;
   int         R1, G1, B1, R3, G3, B3;
   float       blend;
   int         rgb, ii, F1, F2;
   int         Bsize[3];
   int         newcolor[3][256];

   if (Fpaintedits && ! Mdrag) return 0;                                         //  if mouse paint, must be drag
   
   Bsize[0] = 256 / Rcolors;                                                     //  input colors per output color
   Bsize[1] = 256 / Gcolors;
   Bsize[2] = 256 / Bcolors;

   for (rgb = 0; rgb < 3; rgb++)                                                 //  loop RGB colors
   for (ii = 0; ii < 256; ii++)                                                  //  loop color value 0-255
   {
      F1 = Bsize[rgb];                                                           //  bucket size    e.g. 64
      F2 = ii / F1 * F1;                                                         //  bucket number  e.g. 0, 1, 2, 3
      if (F1 < 256) 
         F2 = F2 + (1.0 * F2 / (256 - F1)) * (F1 - 1);                           //  bucket value   e.g. 0, 
      if (F2 > 255) F2 = 255;
      if (F2 < 0) F2 = 0;
      newcolor[rgb][ii] = F2;                                                    //  map input color to output color
   }
   
   get_edit_pixels_init(1,0);                                                    //  initz. pixel loop

   while (true)                                                                  //  loop all edit pixels
   {
      Fend = get_edit_pixels(0,px,py,blend);
      if (Fend) break;
      if (blend == 0) continue;

      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      
      R1 = pix1[0];                                                              //  input RGB values, 0-255
      G1 = pix1[1];
      B1 = pix1[2];
      
      R3 = newcolor[0][R1];
      G3 = newcolor[1][G1];
      B3 = newcolor[2][B1];

      if (Fpaintedits)                                                           //  gradual edit within mouse circle
      {
         if (blend > 0) 
         {
            pix3[0] = blend * R3 + (1-blend) * pix3[0];                          //  pix3 --> mix reduced colors and pix3
            pix3[1] = blend * G3 + (1-blend) * pix3[1];
            pix3[2] = blend * B3 + (1-blend) * pix3[2];
         }
         else if (blend < 0) 
         {
            pix3[0] = -blend * pix1[0] + (1+blend) * pix3[0];                    //  pix3 --> pix1
            pix3[1] = -blend * pix1[1] + (1+blend) * pix3[1];
            pix3[2] = -blend * pix1[2] + (1+blend) * pix3[2];
         }
      }

      else                                                                       //  full edit for image or area
      {
         pix3[0] = blend * R3 + (1-blend) * pix1[0];                             //  pix3 --> mix reduced colors and pix1 
         pix3[1] = blend * G3 + (1-blend) * pix1[1];
         pix3[2] = blend * B3 + (1-blend) * pix1[2];
      }
   }

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

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

   return 0;
}


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

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

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


//  menu function

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

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

   F1_help_topic = "shift colors";

   Plog(1,"m_shift_colors \n");

   EFshiftcolors.menuname = "Shift Colors";
   EFshiftcolors.menufunc = m_shift_colors;
   EFshiftcolors.FprevReq = 1;                                                   //  use preview
   EFshiftcolors.Farea = 2;                                                      //  select area OK
   EFshiftcolors.Fscript = 1;                                                    //  scripting supported
   EFshiftcolors.Fpaintedits = 1;                                                //  can use with paint edits
   EFshiftcolors.threadfunc = shiftcolors_thread;                                //  thread function

   if (! edit_setup(EFshiftcolors)) return;                                      //  setup edit
   
/***
       ______________________________________
      |           Shift Colors               |
      |                                      |
      | red:    green =======[]======= blue  |
      | green:   blue ==========[]==== red   |
      | blue:     red ======[]======== green |
      | all:      all ========[]====== all   |
      |                                      |
      |                     [Reset] [OK] [X] |
      |______________________________________|

***/

   zdialog *zd = zdialog_new("Shift Colors",Mwin,"Reset","OK"," X ",null);
   EFshiftcolors.zd = zd;

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

   zdialog_add_widget(zd,"label","labr","vb1","Red");
   zdialog_add_widget(zd,"label","labr","vb1","Green");
   zdialog_add_widget(zd,"label","labr","vb1","Blue");
   zdialog_add_widget(zd,"label","labr","vb1","All");

   zdialog_add_widget(zd,"label","labg","vb2","Green");
   zdialog_add_widget(zd,"label","labb","vb2","Blue");
   zdialog_add_widget(zd,"label","labr","vb2","Red");
   zdialog_add_widget(zd,"label","laba","vb2","All");

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

   zdialog_add_widget(zd,"label","labb","vb4","Blue");
   zdialog_add_widget(zd,"label","labr","vb4","Red");
   zdialog_add_widget(zd,"label","labg","vb4","Green");
   zdialog_add_widget(zd,"label","laba","vb4","All");

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

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


//  shift color dialog event and completion function

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

   float    shiftall;

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   
   if (strmatch(event,"fullsize")) {                                             //  from select area
      if (! CEF->Fpreview) return 1;
      edit_fullsize();
      thread_signal();
      thread_wait();                                                             //  required for paint edits
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  reset 
         zd->zstat = 0;                                                          //  keep dialog active
         zdialog_stuff(zd,"red",0.5);
         zdialog_stuff(zd,"green",0.5);
         zdialog_stuff(zd,"blue",0.5);
         zdialog_stuff(zd,"all",0.5);
         edit_reset();
         return 1;
      }

      if (zd->zstat == 2) {                                                      //  done
         edit_fullsize();                                                        //  get full size image
         thread_signal();
         edit_addhist("red:%.3f green:%.3f blue:%.3f",                           //  edit parms > edit hist
                        shiftred,shiftgreen,shiftblue); 
         edit_done(0);                                                           //  commit edit
      }

      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }
   
   if (strmatch(event,"focus")) return 1;

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

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

   return 1;
}


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

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

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

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

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

   do_wthreads(shiftcolors_wthread,NSMP);                                        //  do worker threads

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

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


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

   int         index = *((int *) (arg));
   int         px, py, Fend;
   float       *pix1, *pix3;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       lossR, lossG, lossB, gainR, gainG, gainB;
   float       shift, move;
   float       blend;
   
   while (true)
   {
      Fend = get_edit_pixels(index,px,py,blend);

      if (Fend) break;
      if (blend == 0) continue;

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

      R1 = pix1[0];
      G1 = pix1[1];
      B1 = pix1[2];

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

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

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

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

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

      R9 = R1 - lossR + gainR;                                                   //  output pixel
      G9 = G1 - lossG + gainG;
      B9 = B1 - lossB + gainB;

      RGBfix(R9,G9,B9);

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

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

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

   return 0;
}


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

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

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


//  menu function

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

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

   F1_help_topic = "alien colors";

   Plog(1,"m_alien_colors \n");

   EFaliencolors.menuname = "Alien Colors";
   EFaliencolors.menufunc = m_alien_colors;
   EFaliencolors.Farea = 2;                                                      //  select area usable
   EFaliencolors.threadfunc = alien_colors_thread;
   //  preview mode not used - do not replace pattern when [OK] pressed

   if (! edit_setup(EFaliencolors)) return;

/***
          __________________________
         |      Alien Colors        |
         |                          |
         |  blocksize  [___]        |
         |  amplitude  [___]        |
         |                          |
         |         [Apply] [OK] [X] |                                            //  24.60
         |__________________________|

***/

   zdialog *zd = zdialog_new("Alien Colors",Mwin,"Apply","OK"," X ",null);
   CEF->zd = zd;
   zdialog_add_widget(zd,"hbox","hbbsz","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labbsz","hbbsz","blocksize","space=5");
   zdialog_add_widget(zd,"zspin","BLsz","hbbsz","10|1000|1|100");
   zdialog_add_widget(zd,"hbox","hbamp","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labamp","hbamp","amplitude","space=5");
   zdialog_add_widget(zd,"zspin","Ampl","hbamp","0.0|1.0|0.01|1.0");

   zdialog_load_inputs(zd);                                                      //  restore previous inputs
   zdialog_run(zd,alien_colors_dialog_event,"save");                             //  run dialog - parallel
   
   zdialog_send_event(zd,"BLsz");                                                //  initial image
   return;
}


//  alien dialog event and completion function

int alien_colors_dialog_event(zdialog *zd, ch *event)                            //  alien dialog event function
{
   using namespace alien_colors_names;

   if (strmatch(event,"focus")) return 1;
   if (strmatch(event,"done")) zd->zstat = 1;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  from f_open()
   
   if (zstrstr("BLsz Ampl",event))
   {
      zdialog_fetch(zd,"BLsz",BLsz);                                             //  get user inputs
      zdialog_fetch(zd,"Ampl",Ampl);
      thread_signal();                                                           //  calculate
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  [Apply]                               24.60
         thread_signal();
         zd->zstat = 0;
      }
      else if (zd->zstat == 2) edit_done(0);                                     //  [OK]
      else edit_cancel(0);                                                       //  [ X ]
   }

   return 1;
}


//  thread function - multiple working threads to update image

void * alien_colors_thread(void *)
{
   using namespace alien_colors_names;

   void * alien_colors_wthread(void *);

   for (int ii = 0; ii < maxblocks; ii++)                                        //  populate random values
   for (int jj = 0; jj < maxblocks; jj++)
      Frand[ii][jj] = drandz();

   do_wthreads(alien_colors_wthread,NSMP);                                       //  worker threads

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

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


void * alien_colors_wthread(void *arg)                                           //  working threads
{
   using namespace alien_colors_names;

   int      index = *((int *) (arg));
   int      px, py;
   int      row, col, row1, col1, row2, col2;
   int      maxrow, maxcol, bsize, edist = 0;
   float    d1, d7, dx, dy;
   float    W0, W1, W2, W3, W4, W5, W6, W7, W8, Wsum;
   float    R1, G1, B1, R3, G3, B3, H, S, L;
   float    theta;
   float    *pix1, *pix3;

   bsize = BLsz;                                                                 //  block size
   if (Eww / bsize >= maxblocks) bsize = Eww / (maxblocks - 1);                  //  stop > maxblocks rows or cols
   if (Ehh / bsize >= maxblocks) bsize = Ehh / (maxblocks - 1);
   
   maxrow = 1 + Ehh / bsize;
   maxcol = 1 + Eww / bsize;
   
   for (py = index; py < Ehh; py += NSMP)
   for (px = 0; px < Eww; px++)
   {
      if (sa_stat == sa_stat_fini) {                                             //  select area active
         int ii = py * Eww + px;
         edist = sa_pixmap[ii];                                                  //  distance from edge
         if (! edist) continue;                                                  //  pixel outside area
      }
      
      d1 = py - py / bsize * bsize;                                              //  y coordinate within block
      d7 = px - px / bsize * bsize;                                              //  x coordinate

      d1 = d1 / bsize;                                                           //  rescale, 0 .. 1
      d7 = d7 / bsize;
      
      dx = d7 - 0.5;                                                             //  distance from pixel to center
      dy = d1 - 0.5;                                                             //    of 9 block group
      W0 = sqrtf(dx*dx + dy*dy);

      dx = d7 - 0.5;
      dy = d1 + 0.5;
      W1 = sqrtf(dx*dx + dy*dy);

      dx = 1.5 - d7;
      dy = d1 + 0.5;
      W2 = sqrtf(dx*dx + dy*dy);

      dx = 1.5 - d7;
      dy = 0.5 - d1;
      W3 = sqrtf(dx*dx + dy*dy);

      dx = 1.5 - d7;
      dy = 1.5 - d1;
      W4 = sqrtf(dx*dx + dy*dy);
      
      dx = 0.5 - d7;
      dy = 1.5 - d1;
      W5 = sqrtf(dx*dx + dy*dy);

      dx = d7 + 0.5;
      dy = 1.5 - d1;
      W6 = sqrtf(dx*dx + dy*dy);
      
      dx = d7 + 0.5;
      dy = 0.5 - d1;
      W7 = sqrtf(dx*dx + dy*dy);

      dx = d7 + 0.5;
      dy = d1 + 0.5;
      W8 = sqrtf(dx*dx + dy*dy);
      
      W0 = 1.5 - W0;                                                             //  block weight:
      W1 = 1.5 - W1;                                                             //    max. distance - actual
      W2 = 1.5 - W2;                                                             //  closer blocks have more weight
      W3 = 1.5 - W3;
      W4 = 1.5 - W4;
      W5 = 1.5 - W5;
      W6 = 1.5 - W6;
      W7 = 1.5 - W7;
      W8 = 1.5 - W8;
      
      if (W2 < 0) W2 = 0;                                                        //  corners may go negative
      if (W4 < 0) W4 = 0;
      if (W6 < 0) W6 = 0;
      if (W8 < 0) W8 = 0;
      
      Wsum = W0 + W1 + W2 + W3 + W4 + W5 + W6 + W7 + W8;                         //  sum of weights
      
      W0 = W0 / Wsum;                                                            //  normalize to make sum = 1
      W1 = W1 / Wsum;
      W2 = W2 / Wsum;
      W3 = W3 / Wsum;
      W4 = W4 / Wsum;
      W5 = W5 / Wsum;
      W6 = W6 / Wsum;
      W7 = W7 / Wsum;
      W8 = W8 / Wsum;
      
      row = py / bsize;                                                          //  block[row][col] for this px/py
      col = px / bsize;
      
      row1 = row2 = row;
      col1 = col2 = col;
      if (row > 0) row1 = row - 1;
      if (row < maxrow) row2 = row + 1;
      if (col > 0) col1 = col - 1;
      if (col < maxcol) col2 = col + 1;
      
      W0 = W0 * Frand[col][row];                                                 //  compute random for pixel from
      W1 = W1 * Frand[col][row1];                                                //    block randoms * weights
      W2 = W2 * Frand[col2][row1];
      W3 = W3 * Frand[col2][row];
      W4 = W4 * Frand[col2][row2];
      W5 = W5 * Frand[col][row2];
      W6 = W6 * Frand[col1][row2];
      W7 = W7 * Frand[col1][row];
      W8 = W8 * Frand[col1][row1];
      
      theta = W0 + W1 + W2 + W3 + W4 + W5 + W6 + W7 + W8;                        //  hue angle change
      theta = theta * 2.0 * Ampl - Ampl;                                         //  scale -Ampl ... +Ampl
   
      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      R1 = pix1[0];
      G1 = pix1[1];
      B1 = pix1[2];
      
      RGBtoHSL(0.0039*R1, 0.0039*G1, 0.0039*B1, H,S,L);
      H = H + 180 * theta;                                                       //  displace H by -90 ... +90
      if (H < 0) H += 360.0;
      else if (H >= 360.0) H -= 360.0;
      HSLtoRGB(H,S,L,R3,G3,B3);
      
      R3 *= 255.0;
      G3 *= 255.0;
      B3 *= 255.0;
      
      pix3[0] = R3;
      pix3[1] = G3;
      pix3[2] = B3;
   }

   return 0;                                                                     //  exit thread
}


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

//  process image using a custom kernel

namespace anykernel_names
{
   int         kernsize = 5;                                                     //  kernel size, N x N
   float       fmpyer = 1.0;                                                     //  multiplier
   int         fadder = 0;                                                       //  adder
   int         kernel[15][15];                                                   //  up to 15 x 15
   editfunc    EFanykernel;
}


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

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

   F1_help_topic = "custom kernel";

   Plog(1,"m_anykernel \n");

   EFanykernel.menuname = "Custom Kernel";
   EFanykernel.menufunc = m_anykernel;
   EFanykernel.Farea = 2;                                                        //  select area usable
   EFanykernel.Fpaintedits = 1;                                                  //  use with paint edits OK               24.50
   EFanykernel.Fscript = 1;                                                      //  scripting supported
   EFanykernel.threadfunc = anykernel_thread;                                    //  thread function

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

   anykernel_make_dialog();
   return;
}


//  build the input dialog with the requested array size

int anykernel_make_dialog()
{
   using namespace anykernel_names;

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

   zdialog  *zd;
   int      row, col;
   ch       rowname[16], cellname[30];

/***
       __________________________________________
      |              Custom Kernel               |
      |                                          |
      |  Kernel size [___]                       |
      |                                          |
      |  [____] [____] [____] [____] [____] ...  |
      |  [____] [____] [____] [____] [____] ...  |
      |  [____] [____] [____] [____] [____] ...  |
      |  [____] [____] [____] [____] [____] ...  |
      |  [____] [____] [____] [____] [____] ...  |
      |  [____] [____] [____] [____] [____] ...  |
      |   ...    ...    ...    ...    ...        |
      |                                          |
      |  multiply [____]   add [____]            |
      |                                          |
      |  Data file  [Load] [Save]                |
      |                                          |
      |                 [Reset] [Apply] [OK] [X] |
      |__________________________________________|

***/

   zd = zdialog_new("Custom Kernel",Mwin,"Reset","Apply","OK"," X ",null);
   EFanykernel.zd = zd;

   zdialog_add_widget(zd,"hbox","hbkern","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labkern","hbkern","Kernel size","space=3");
   zdialog_add_widget(zd,"zspin","kernsize","hbkern","3|15|2|5","size=3");
   zdialog_stuff(zd,"kernsize",kernsize);

   for (row = 1; row <= kernsize; row++)                                         //  make entry cell matrix
   {
      snprintf(rowname,16,"row%02d",row);
      zdialog_add_widget(zd,"hbox",rowname,"dialog","space=5");

      for (col = 1; col <= kernsize; col++)
      {
         snprintf(cellname,30,"cell%02d%02d",col,row);
         zdialog_add_widget(zd,"zspin",cellname,rowname,"-99|+99|1|0","size=4|space=4");
      }
   }

   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=5");                     //  multiplier and adder
   zdialog_add_widget(zd,"label","labmul","hbf","multiply","space=3");
   zdialog_add_widget(zd,"zspin","fmul","hbf","0|99|0.01|1","size=5");
   zdialog_add_widget(zd,"label","space","hbf",0,"space=3");
   zdialog_add_widget(zd,"label","labadd","hbf","add","space=3");
   zdialog_add_widget(zd,"zspin","fadd","hbf","-999|+999|1|0","size=5");

   zdialog_add_widget(zd,"hbox","hbfile","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labfile","hbfile","Data file","space=3");
   zdialog_add_widget(zd,"button","load","hbfile","Load","space=3");
   zdialog_add_widget(zd,"button","save","hbfile","Save","space=3");
   
   zdialog_add_widget(zd,"hbox","hbfname","dialog",0,0);
   zdialog_add_widget(zd,"label","file","hbfname","File:","space=3");
   zdialog_add_widget(zd,"label","filename","hbfname",0,"space=3");

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


//  dialog event and completion callback function

int anykernel_dialog_event(zdialog *zd, ch *event)
{
   using namespace anykernel_names;

   int      row, col, err, nn, kernsize2;
   float    value;
   ch       cellname[20];
   ch       *filename, dirname[200];
   FILE     *fid;
   ch       *mess = "Load settings from file";
   ch       *pp;
   
   if (strmatch(event,"focus")) return 1;
   if (strmatch(event,"apply")) zd->zstat = 2;                                   //  from script
   if (strmatch(event,"paint")) zd->zstat = 2;                                   //  from paint edits mouse                24.50
   if (strmatch(event,"done")) zd->zstat = 3;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 4;                                  //  from f_open()

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  reset 
      {
         zd->zstat = 0;                                                          //  keep dialog active
         edit_reset();
         return 1;
      }

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

         for (row = 1; row <= kernsize; row++)                                   //  get kernel values from dialog
         for (col = 1; col <= kernsize; col++)
         {
            snprintf(cellname,20,"cell%02d%02d",col,row);
            zdialog_fetch(zd,cellname,value);
            kernel[row-1][col-1] = value;
         }
         
         zdialog_fetch(zd,"fmul",fmpyer);
         zdialog_fetch(zd,"fadd",fadder);

         thread_signal();                                                        //  start thread
         return 1;
      }

      if (zd->zstat == 3)                                                        //  done - commit edit 
      {
         zdialog_save_last_widgets(zd,null,"custom_kernel");
         edit_done(0);
      }
      
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   if (strmatch(event,"kernsize"))                                               //  change kernel size
   {
      zdialog_fetch(zd,"kernsize",kernsize2);
      if (kernsize2 == kernsize) return 1;
      kernsize = kernsize2;
      zdialog_free(zd);
      anykernel_make_dialog();
      return 1;
   }

   if (strmatch(event,"save"))                                                   //  save kernel data to a file
      zdialog_save_widgets(zd,null,"custom_kernel",null);

   if (strmatch(event,"load"))                                                   //  load kernel data from a file 
   {
      snprintf(dirname,200,"%s/custom_kernel",get_zhomedir());                   //  folder for data files
      filename = zgetfile(mess,MWIN,"file",dirname,0);                           //  open data file
      if (! filename) return 1;                                                  //  user cancel

      fid = fopen(filename,"r");                                                 //  open file
      if (! fid) {
         zmessageACK(Mwin,"%s \n %s",filename,strerror(errno));
         zfree(filename);
         return 1;
      }

      nn = fscanf(fid,"kernsize == %d",&kernsize2);                              //  read 1st record, "kernsize == N"
      fclose(fid);

      if (nn != 1) {
         zmessageACK(Mwin,"file format error: \n %s",filename);
         zfree(filename);
         return 1;
      }      

      if (kernsize2 != kernsize) {                                               //  change kernel size to match file
         zdialog_free(zd);
         kernsize = kernsize2;
         anykernel_make_dialog();
         zd = EFanykernel.zd;                                                    //  zd has changed
      }
      
      fid = fopen(filename,"r");                                                 //  re-open file and load kernel data
      err = zdialog_load_widgets(zd,null,"custom_kernel",fid);
      if (err) zmessageACK(Mwin,"file format error: \n %s",filename);
      fclose(fid);
      
      pp = strrchr(filename,'/');
      zdialog_stuff(zd,"filename",pp+1);
      
      zfree(filename);
   }

   return 1;
}


//  image anykernel thread function

void * anykernel_thread(void *)
{
   using namespace anykernel_names;

   void * anykernel_wthread(void *arg);
   
   int   rad = kernsize / 2;
   
   if (Fpaintedits && ! Mdrag) return 0;                                         //  if mouse paint, must be drag          24.50

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

   do_wthreads(anykernel_wthread,NSMP);                                          //  worker threads
   
   CEF->Fmods++;
   CEF->Fsaved = 0;

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

   return 0;
}


void * anykernel_wthread(void *arg)                                              //  worker thread function
{
   using namespace anykernel_names;

   int      index = *((int *) arg);
   int      px, py, qx, qy;
   int      rad, Fend;
   float    kval, blend;
   float    R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float    *pix1, *pix3, *pix9;

   rad = kernsize / 2;                                                           //  image margins, not processed

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

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

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

      R9 = G9 = B9 = 0;                                                          //  new output RGB

      for (qy = -rad; qy <= +rad; qy++)                                          //  loop pixels around (px,py) 
      for (qx = -rad; qx <= +rad; qx++)                                          //    mapped to kernel
      {
         pix9 = PXMpix(E1pxm,px+qx,py+qy);                                       //  pixel
         kval = kernel[qy+rad][qx+rad];                                          //  kernel value for pixel
         R9 += kval * pix9[0];                                                   //  sum (kernel * pixel value) per RGB 
         G9 += kval * pix9[1];
         B9 += kval * pix9[2];
      }

      if (fmpyer != 1.0) {                                                       //  apply multiplier
         R9 = fmpyer * R9;
         G9 = fmpyer * G9;
         B9 = fmpyer * B9;
      }
      
      if (fadder != 0) {                                                         //  apply adder
         R9 += fadder;
         G9 += fadder;
         B9 += fadder;
      }

      RGBfix(R9,G9,B9);                                                          //  output RGB

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

         else if (blend < 0)
         {
            R3 = -blend * R1 + (1+blend) * R3;                                   //  decrease edit
            G3 = -blend * G1 + (1+blend) * G3;
            B3 = -blend * B1 + (1+blend) * B3;
         }
      }

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

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

   return 0;
}


