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

   Fotocx - edit photos and manage collections

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

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

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

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

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

   m_outlines              convert image to outline drawing
   m_sketch1               convert photo to simulated sketch
   m_sketch2               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_fusion                suppress fine details while retaining sharpness
   m_pattern               tile an image with a repeating pattern
   m_crosshatch            convert photo to crosshatch sketch
   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 or color lines)
   m_addglare              add glare around bright objects
   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)
{
   using namespace outlines_names;

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

   F1_help_topic = TX("OUTLINES");

   printf("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 [__]    |
      | Low Amplify: [__]                 |
      | Mode: A (o)  B (o)                |
      | [_] black/white  [_] negative     |
      |                                   |
      |                          [OK] [X] |
      |___________________________________|

***/

   zdialog *zd = zdialog_new(TX("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",TX("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",TX("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");
   zdialog_add_widget(zd,"label","labamp","hbA",TX("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");
   zdialog_add_widget(zd,"label","labM","hbM",TX("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",TX("black/white"),"space=5");
   zdialog_add_widget(zd,"label","space","hb4",0,"space=10");
   zdialog_add_widget(zd,"check","Fneg","hb4",TX("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
   }

   return 0;
}


void * outlines_wthread2(void *arg)                                                    //  draw image
{
   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.05 * Amp;                                                                   //  25.1
   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

      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
      if (ModeB) F2 = 1.0;                                                             //  use flat method

      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 sketch1_names
{
   editfunc    EFsketch1;                                                              //  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_sketch1(GtkWidget *, ch *menu)
{
   using namespace sketch1_names;

   int    sketch1_dialog_event(zdialog* zd, ch *event);
   void * sketch1_thread(void *);

   int      ii, px, py;
   float    *pix0, *pix1, *pix2, *pix3, *pix4;
   float    f1, f2;

   F1_help_topic = TX("SKETCH1");

   printf("m_sketch1 \n");

   EFsketch1.menuname = "Sketch1";
   EFsketch1.menufunc = m_sketch1;
   EFsketch1.Farea = 2;                                                                //  select area usable
   EFsketch1.Fscript = 1;                                                              //  scripting supported
   EFsketch1.threadfunc = sketch1_thread;                                              //  thread function

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

   cc = Eww * Ehh;

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

   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
   }

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

***/

   zdialog *zd = zdialog_new(TX("Sketch1"),Mwin,"OK","X",null);                        //  sketch dialog
   EFsketch1.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labbrite","hb1",TX("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",TX("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",TX("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",TX("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",TX("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",TX("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,sketch1_dialog_event,"save");                                        //  run dialog - parallel
   zdialog_send_event(zd,"init");

   return;
}


//  sketch1 dialog event and completion function

int sketch1_dialog_event(zdialog *zd, ch *event)
{
   using namespace sketch1_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 * sketch1_thread(void *)
{
   using namespace sketch1_names;

   void sketch1_wthread();

   progress_setgoal(255);
   sketch1_wthread();
   progress_setgoal(0);

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

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


//  working thread

void sketch1_wthread()
{
   using namespace sketch1_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 a photo or other image into a simulated pencil sketch

namespace sketch2_names
{
   editfunc    EFsketch2;
   int         *conmap;                                                                //  image contrast map
   float       Fblend;                                                                 //  image blend, 0 - 1.0
}


//  menu function

void m_sketch2(GtkWidget *, ch *menu)                                                  //  26.0 
{
   using namespace sketch2_names;

   int    sketch2_dialog_event(zdialog* zd, ch  *event);
   void   sketch2_curvedit(int spc);
   void * sketch2_thread(void *);

   int         ii, cc, px, py, qx, qy;
   float       *pix1, *pix2;
   int         R1, G1, B1, R2, G2, B2, con1, con2;

   printf("m_sketch2 \n");

   F1_help_topic = TX("SKETCH2");

   EFsketch2.menuname = "Sketch2";
   EFsketch2.menufunc = m_sketch2;
   EFsketch2.Farea = 2;                                                                //  select area usable
   EFsketch2.Fpaintedits = 1;                                                          //  allow paint edits
   EFsketch2.Fscript = 1;                                                              //  allow scripting
   EFsketch2.threadfunc = sketch2_thread;

   if (! edit_setup(EFsketch2)) return;                                                //  setup edit
   
   cc = Eww * Ehh * sizeof(int);                                                       //  allocate contrast map
   conmap = (int *) zmalloc(cc,"m_sketch2");
   
   for (py = 1; py < Ehh-1; py++)                                                      //  map RGB contrast of all pixels
   for (px = 1; px < Eww-1; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);                                                      //  pixel to map
      R1 = pix1[0];                                                                    //  RGB values 0-255
      G1 = pix1[1];
      B1 = pix1[2];
      con1 = 0;

      for (qy = py-1; qy <= py+1; qy++)                                                //  loop 8 neighbor pixels
      for (qx = px-1; qx <= px+1; qx++)
      {
         if (qx == px && qy == py) continue;                                           //  skip central pixel = self

         pix2 = PXMpix(E1pxm,qx,qy);                                                   //  neighbor pixel
         R2 = pix2[0];                                                                 //  RGB values 0-255
         G2 = pix2[1];
         B2 = pix2[2];
         
         con2 = abs(R2-R1) + abs(G2-G1) + abs(B2-B1);                                  //  total RGB contrast
         if (con2 > con1) con1 = con2;                                                 //  save largest value
      }
         
      ii = py * Eww + px;                                                              //  pix1 contrast
      if (con1 < 0) con1 = 0;
      if (con1 > 765) con1 = 765;                                                      //  guard poss. bad RGB data
      conmap[ii] = con1;
   }

/***
       _______________________________________________
      |                   Sketch2                     |
      |  ___________________________________________  |
      | |                                           | |
      | |             curve edit area               | |
      | |                                           | |
      | | ----------------------------------------- | |                                //  editable power curve
      | |                                           | |
      | |                                           | |
      | |___________________________________________| |
      |  low contrast                          high   |                                //  contrast scale
      |                                               |
      |  Blend: 0% ============[]============== 100%  |                                //  blend input and output images
      |                                               |
      |                              [Reset] [OK] [X] |
      |_______________________________________________|

***/

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

   zdialog_add_widget(zd,"frame","frameH","dialog",0,"expand");                        //  edit-curve
   GtkWidget *frameH = zdialog_gtkwidget(zd,"frameH");
   spldat *sd = splcurve_init(frameH,sketch2_curvedit);
   EFsketch2.sd = sd;

   sd->Nscale = 1;                                                                     //  horizontal line, neutral value
   sd->xscale[0][0] = 0.00;
   sd->yscale[0][0] = 0.50;
   sd->xscale[1][0] = 1.00;
   sd->yscale[1][0] = 0.50;

   sd->nap[0] = 3;                                                                     //  initial curve is neutral
   sd->vert[0] = 0;
   sd->apx[0][0] = 0.01;                                                               //  horizontal line
   sd->apy[0][0] = 0.50;
   sd->apx[0][1] = 0.50;
   sd->apy[0][1] = 0.50;                                                               //  curve 0 = power
   sd->apx[0][2] = 0.99;
   sd->apy[0][2] = 0.50;
   splcurve_generate(sd,0);
   sd->mod[0] = 0;                                                                     //  mark curve unmodified

   sd->Nspc = 1;                                                                       //  1 curve
   sd->fact[0] = 1;                                                                    //  curve 0 active

   zdialog_add_widget(zd,"hbox","hbcon","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","lowcon","hbcon",TX("low contrast"),"space=3");
   zdialog_add_widget(zd,"label","space","hbcon",0,"expand");
   zdialog_add_widget(zd,"label","hicon","hbcon",TX("high"));
   
   zdialog_add_widget(zd,"hbox","hblend","dialog",0,"space=10");                       //  blend slider, 0 - 1.0
   zdialog_add_widget(zd,"label","lab0","hblend",TX("Blend: 0%"),"space=3");
   zdialog_add_widget(zd,"hscale","Fblend","hblend","0.0|1.0|0.01|0.0","expand");
   zdialog_add_widget(zd,"label","lab100","hblend","100%","space=3");

   zdialog_resize(zd,250,300);
   zdialog_run(zd,sketch2_dialog_event,"save");                                        //  run dialog - parallel
   
   Fblend = 0;
   thread_signal();

   return;
}


//  dialog event and completion callback function

int sketch2_dialog_event(zdialog *zd, ch  *event)
{
   using namespace sketch2_names;

   spldat      *sd = EFsketch2.sd;

   if (strmatch(event,"done")) zd->zstat = 2;                                          //  apply and quit
   if (strmatch(event,"cancel")) zd->zstat = 3;                                        //  cancel

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

      sd->nap[0] = 3;                                                                  //  curve is neutral
      sd->vert[0] = 0;
      sd->apx[0][0] = 0.01;
      sd->apy[0][0] = 0.50;
      sd->apx[0][1] = 0.50;
      sd->apy[0][1] = 0.50;
      sd->apx[0][2] = 0.99;
      sd->apy[0][2] = 0.50;
      splcurve_generate(sd,0);
      sd->mod[0] = 0;                                                                  //  mark curve unmodified

      gtk_widget_queue_draw(sd->drawarea);                                             //  redraw curve

      Fblend = 0;                                                                      //  no image blend
      zdialog_stuff(zd,"Fblend",0);

      edit_reset();
      return 1;
   }

   if (zd->zstat == 2)                                                                 //  [OK]
   {
      freeMouse();                                                                     //  free mouse if curve edit active

      if (CEF->Fmods) {
         edit_addhist("blend:%.3f",Fblend);                                            //  edit parms > edit hist
         edit_done(0);
      }
      else edit_cancel(0);                                                             //  no change
      zfree(conmap);
      return 1;
   }

   if (zd->zstat < 0 || zd->zstat > 2) {                                               //  cancel
      edit_cancel(0);
      zfree(conmap);
      return 1;
   }
   
   if (strmatch(event,"Fblend")) {                                                     //  new blend ratio
      zdialog_fetch(zd,"Fblend",Fblend);
      thread_signal();
   }

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


//  this function is called when the curve is edited

void sketch2_curvedit(int spc)
{
   using namespace sketch2_names;
   thread_signal();
   return;
}


//  thread function

void * sketch2_thread(void *arg)
{
   using namespace sketch2_names;

   void * sketch2_wthread(void *);

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

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

   do_wthreads(sketch2_wthread,NSMP);

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

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


void * sketch2_wthread(void *arg)                                                      //  worker thread function
{
   using namespace sketch2_names;

   int         index = *((int *) arg);
   int         ii, Fend, px, py, con;
   float       blend;
   float       *pix1, *pix3;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       pixbrite, F;
   spldat      *sd = EFsketch2.sd;

   while (true)                                                                        //  loop all edit pixels
   {
      Fend = get_edit_pixels(index,px,py,blend);
      if (Fend) break;
      if (blend == 0) continue;
      
      pix1 = PXMpix(E1pxm,px,py);                                                      //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                      //  output pixel

      R1 = pix1[0];                                                                    //  input RGB values
      G1 = pix1[1];
      B1 = pix1[2];

      R3 = pix3[0];                                                                    //  current output RGB
      G3 = pix3[1];
      B3 = pix3[2];

      ii = py * Eww + px;                                                              //  get pixel contrast
      con = conmap[ii];                                                                //  contrast = 0 ... 765
      ii = 5 * con;                                                                    //  rescale to 0 ... 3825
      if (ii > 999) ii = 999;                                                          //  cut off upper 74%
      F = sd->yval[0][ii];                                                             //  get curve y-val, 0.0 ... 1.0
      F = F * 0.01 * (1000 - ii) + 1;                                                  //  amplify y-val for low x-val
      
      pixbrite = F * con;                                                              //  amplified contrast --> brightness
      if (pixbrite > 255) pixbrite = 255;
      R9 = G9 = B9 = 255 - pixbrite;                                                   //  invert to darkness
      
      if (Fblend > 0)
      {
         R9 = Fblend * R1 + (1.0 - Fblend) * R9;                                       //  blend input image
         G9 = Fblend * G1 + (1.0 - Fblend) * G9;
         B9 = Fblend * B1 + (1.0 - Fblend) * B9;
      }

      if (Fpaintedits)                                                                 //  gradual edit within mouse circle
      {
         if (blend > 0)
         {
            R3 = blend * R9 + (1-blend) * R3;                                          //  increase edit
            G3 = blend * G9 + (1-blend) * G3;
            B3 = blend * B9 + (1-blend) * B3;
         }

         else if (blend < 0)
         {
            R3 = -blend * R1 + (1+blend) * R3;                                         //  decrease edit
            G3 = -blend * G1 + (1+blend) * G3;
            B3 = -blend * B1 + (1+blend) * B3;
         }
      }

      else                                                                             //  full edit for image or 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;
}


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

//  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 = TX("CARTOON");

   printf("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(TX("Cartoon"),Mwin,TX("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",TX("Line Threshold"));
   zdialog_add_widget(zd,"label","lab1","vb1",TX("Line Width"));
   zdialog_add_widget(zd,"label","lab1","vb1",TX("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;
   int      pskip = 0;

   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;

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

   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;
   int      pskip = 1;

   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;

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

   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;
   int         pskip = 0;

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

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

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

   printf("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(TX("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",TX("Radius"),"space=5");
   zdialog_add_widget(zd,"zspin","radius","hb1","0|20|1|2");
   zdialog_add_widget(zd,"label","lab2","hb1",TX("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",TX("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",TX("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
      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 = TX("TILES1");

   printf("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(TX("Make Square Tiles"),Mwin,TX("Apply"),"OK","X",null);
   EFtiles1.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labt","hb1",TX("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",TX("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",TX("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 = TX("TILES2");

   printf("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(TX("Irregular Tiles"),Mwin,TX("Apply"),"OK","X",null);
   EFtiles2.zd = zd;

   zdialog_add_widget(zd,"hbox","hbsize","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labsize","hbsize",TX("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",TX("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",TX("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",TX("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("size:%d depth:%d goal:%d match:%d",                             //  25.1
                        base_size, color_depth, area_goal, color_match);               //  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 = TX("PAINTING");

   printf("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(TX("Painting"),Mwin,TX("Apply"),"OK","X",null);
   EFpainting.zd = zd;

   zdialog_add_widget(zd,"hbox","hbbr","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labbr","hbbr",TX("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",TX("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",TX("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",TX("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",TX("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("radius:%d color:%d patch:%d match:%d borders:%d",
                       blur_radius,color_depth,area_goal,color_match,borders);         //  edit parms > edit hist                25.1
         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 = TX("TEXTURE");

   printf("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(TX("Add Texture"),Mwin,TX("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",TX("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",TX("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;
   int         pskip = 0;

   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;

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

   return 0;                                                                           //  exit thread
}


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

//  suppress fine details while retaining sharpness
//  also useful for noise suppression

namespace fusion_names
{
   editfunc    EFfusion;
   int         Frun;
   int         count;
   zdialog     *zd;
}


//  menu function

void m_fusion(GtkWidget *, ch *menu)                                                   //  25.1
{
   using namespace fusion_names;

   int    fusion_dialog_event(zdialog* zd, ch  *event);
   void * fusion_thread(void *);

   printf("m_fusion \n");

   F1_help_topic = TX("FUSION");

   EFfusion.menuname = "fusion";
   EFfusion.menufunc = m_fusion;
   EFfusion.Farea = 2;                                                                 //  select area usable
   EFfusion.threadfunc = fusion_thread;

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

   E8pxm = PXM_copy(E0pxm);                                                            //  initial input image

/***
          ___________________________________
         |             Fusion                |
         |                                   |
         | [start] [stop] [step]  count: NN  |
         |                                   |
         |                 [Reset] [OK] [X]  |
         |___________________________________|

***/

   zd = zdialog_new(TX("Fusion"),Mwin,TX("Reset"),"OK","X",null);
   EFfusion.zd = zd;
   zdialog_add_widget(zd,"hbox","hbcon","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","start","hbcon",TX("start"),"space=3");
   zdialog_add_widget(zd,"button","stop","hbcon",TX("stop"),"space=3");
   zdialog_add_widget(zd,"button","step","hbcon",TX("step"),"space=3");
   zdialog_add_widget(zd,"label","count","hbcon",TX("count: 0"),"space=5");
   zdialog_add_ttip(zd,"start",TX("start fusion process"));
   zdialog_add_ttip(zd,"stop",TX("stop fusion process"));

   Frun = 0;
   count = 0;
   zdialog_run(zd,fusion_dialog_event,"save");

   return;
}


//  dialog event and completion callback function

int fusion_dialog_event(zdialog *zd, ch  *event)
{
   using namespace fusion_names;

   int  fusion_timerfunc(void *);

   static int N0 = 0, N1 = 1;

   if (strmatch(event,"focus")) return 1;
   if (strmatch(event,"done")) zd->zstat = 2;                                          //  done
   if (strmatch(event,"cancel")) zd->zstat = 3;                                        //  cancel

   thread_wait();

   if (strmatch(event,"start"))
   {
      if (! Frun) {
         Frun = 1;                                                                     //  start timer function
         g_timeout_add(500,fusion_timerfunc,&N0);                                      //  runs every 0.5 sec.
      }
   }

   else if (strmatch(event,"stop")) Frun = 0;

   else if (strmatch(event,"step")) {
      Frun = 1;
      g_timeout_add(500,fusion_timerfunc,&N1);
   }

   else Frun = 0;

   if (zd->zstat) Frun = 0;                                                            //  stop thread

   if (zd->zstat == 1)                                                                 //  [reset]
   {
      zd->zstat = 0;                                                                   //  keep dialog active
      edit_reset();
      PXM_copy(E0pxm,E8pxm);                                                           //  restore input image
      zdialog_stuff(zd,"count",TX("count: 0"));
      count = 0;
      return 1;
   }

   if (zd->zstat == 2)                                                                 //  [OK]
   {
      edit_addhist("count:%d",count);                                                  //  edit parms > edit hist
      PXM_free(E8pxm);                                                                 //  free input image
      edit_done(0);
      return 1;
   }

   if (zd->zstat < 0 || zd->zstat > 2)                                                 //  [X] or [-] cancel
   {
      PXM_free(E8pxm);                                                                 //  free input image
      edit_cancel(0);
      return 1;
   }

   return 1;
}


//  timer function - start thread every interval

int fusion_timerfunc(void *arg)
{
   using namespace fusion_names;

   ch    labcount[20];

   int   N = *((int *) arg);

   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 image window

   snprintf(labcount,20,"count: %d",++count);                                          //  update counter
   zdialog_stuff(zd,"count",labcount);

   if (N == 1) {
      Frun = 0;                                                                        //  run only 1 time
      return 0;                                                                        //  exit forever
   }

   return 1;                                                                           //  repeat every interval
}


//  thread function

void * fusion_thread(void *arg)
{
   using namespace fusion_names;

   void * fusion_median_wthread(void *);
   void * fusion_smooth_wthread(void *);
   void * fusion_KH_wthread(void *);

   get_edit_pixels_init(NSMP,1);                                                       //  initz. pixel loop
   do_wthreads(fusion_median_wthread,NSMP);                                            //  process pixels
   PXM_copy(E3pxm,E8pxm);                                                              //  E3 (output) --> E8 (input for next)

   get_edit_pixels_init(NSMP,1);                                                       //  initz. pixel loop
   do_wthreads(fusion_smooth_wthread,NSMP);                                            //  process pixels
   PXM_copy(E3pxm,E8pxm);                                                              //  E3 (output) --> E8 (input for next)

   get_edit_pixels_init(NSMP,1);                                                       //  initz. pixel loop
   do_wthreads(fusion_KH_wthread,NSMP);                                                //  process pixels
   PXM_copy(E3pxm,E8pxm);                                                              //  E3 (output) --> E8 (input for next)

   return 0;
}


//  pixel RGB = median RGB fof 8 neighbor pixels

void * fusion_median_wthread(void *arg)
{
   int         index = *((int *) arg);
   int         ii, rgb, Fend;
   int         px, py, dx, dy;
   int         nc = E3pxm->nc;
   float       *pix8, *pix3, *pixN, pix9[3];
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       blend, median;
   int16       rgbhist[256];
   int         rgbsum;

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

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

      R1 = pix9[0] = pix8[0];
      G1 = pix9[1] = pix8[1];
      B1 = pix9[2] = pix8[2];

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

      for (rgb = 0; rgb < 3; rgb++)                                                    //  loop all RGB colors
      {
         memset(rgbhist,0,256*sizeof(int16));                                          //  clear rgb histogram
         rgbsum = 0;

         for (dy = -1; dy <= 1; dy++)                                                  //  loop surrounding pixels
         for (dx = -1; dx <= 1; dx++)                                                  //  get RGB values
         {
            pixN = pix8 + (dy * Eww + dx) * nc;                                        //  make histogram of RGB values
            ii = pixN[rgb];
            rgbhist[ii]++;
         }

         for (ii = 0; ii < 256; ii++)                                                  //  sum histogram from 0 to 255
         {
            rgbsum += rgbhist[ii];                                                     //  break when half of RGB values
            if (rgbsum > 4) break;                                                     //    have been counted
         }

         median = ii;
         pix9[rgb] = median;                                                           //  pixel rgb = median
      }

      R9 = pix9[0];
      G9 = pix9[1];
      B9 = pix9[2];

      R3 = blend * R9 + (1-blend) * R1;
      G3 = blend * G9 + (1-blend) * G1;
      B3 = blend * B9 + (1-blend) * B1;

      pix3[0] = R3;                                                                    //  set output pixel
      pix3[1] = G3;
      pix3[2] = B3;
   }

   return 0;
}


//  pixel RGB = mean of two best-match opposing neighbors

void * fusion_smooth_wthread(void *arg)
{
   int         index = *((int *) arg);
   int         Fend, rgb;
   int         px, py, px1, py1, px2, py2;
   float       *pix8, *pix3;
   float       *pixA, *pixB;
   float       *pix91, *pix92, pix9[3];
   float       blend;
   int         bmpx, bmpy;
   float       diffA, diffB, diffAB, mindiff, mrgb;

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

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

      for (rgb = 0; rgb < 3; rgb++)                                                    //  loop each RGB color
      {
         mindiff = 999;                                                                //  remember best matching pixel pair
         bmpx = bmpy = 0;

         py1 = -1;                                                                     //  py1 = top row
         py2 = +1;                                                                     //  py2 = bottom row

         for (px1 = -1; px1 <= +1; px1++)                                              //  px1/py1 = top row pixel
         {
            px2 = -px1;                                                                //  px2/py2 = opposite bottom row pixel

            pixA = PXMpix(E8pxm,px+px1,py+py1);                                        //  top row pixel
            pixB = PXMpix(E8pxm,px+px2,py+py2);                                        //  bottom row pixel

            diffA = fabsf(pixA[rgb] - pix8[rgb]);                                      //  diff from pix8
            diffB = fabsf(pixB[rgb] - pix8[rgb]);                                      //  diff from pix8

            diffAB = diffA + diffB;                                                    //  match both with px/py
            if (diffAB < mindiff) {
               mindiff = diffAB;                                                       //  remember best match
               bmpx = px1;
               bmpy = py1;
            }
         }

         px1 = -1;                                                                     //  px1 = left col
         px2 = +1;                                                                     //  px2 = right col
         py1 = py2 = 0;

         pixA = PXMpix(E8pxm,px+px1,py+py1);                                           //  left col pixel
         pixB = PXMpix(E8pxm,px+px2,py+py2);                                           //  right col pixel

         diffA = fabsf(pixA[rgb] - pix8[rgb]);                                         //  diff from pix8
         diffB = fabsf(pixB[rgb] - pix8[rgb]);                                         //  diff from pix8

         diffAB = diffA + diffB;                                                       //  match both with px/py
         if (diffAB < mindiff) {
            mindiff = diffAB;                                                          //  remember best match
            bmpx = px1;
            bmpy = py1;
         }

         px1 = bmpx;                                                                   //  best matching pixel pair
         py1 = bmpy;                                                                   //  (relative to px/py)
         px2 = -px1;
         py2 = -py1;

         px1 += px;                                                                    //  absolute values
         py1 += py;
         px2 += px;
         py2 += py;

         pix91 = PXMpix(E8pxm,px1,py1);                                                //  opposing pixel pair
         pix92 = PXMpix(E8pxm,px2,py2);
         mrgb = 0.5 * (pix91[rgb] + pix92[rgb]);                                       //  mean RGB value
         pix9[rgb] = mrgb;                                                             //  rgb = mean

         pix3[rgb] = blend * pix9[rgb] + (1-blend) * pix8[rgb];                        //  output pixel
      }
   }

   return 0;
}


//  pixel RGB = neighbor pixel with least variance of neighborhood (Kuwahara sharpen)

void * fusion_KH_wthread(void *arg)
{
   float       *pix8, *pix3;
   int         px, py, qx, qy, rx, ry, Fend;
   float       red, green, blue, red2, green2, blue2;
   float       vmin, vall, vred, vgreen, vblue;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       blend;

   int      index = *((int *) arg);

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

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

      R1 = R9 = pix8[0];                                                               //  input image RGB values
      G1 = G9 = pix8[1];
      B1 = B9 = pix8[2];

      R3 = pix3[0];                                                                    //  current output image RGB
      G3 = pix3[1];
      B3 = pix3[2];

      vmin = 99999;
      R9 = G9 = B9 = 0;

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

         for (ry = qy; ry <= qy+1; ry++)                                               //  loop all pixels in neighborhood
         for (rx = qx; rx <= qx+1; 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 / 4;                                                                //  mean RGB of neighborhood
         green = green / 4;
         blue = blue / 4;

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

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

      R3 = blend * R9 + (1-blend) * R1;
      G3 = blend * G9 + (1-blend) * G1;
      B3 = blend * B9 + (1-blend) * B1;

      pix3[0] = R3;                                                                    //  set output pixel
      pix3[1] = G3;
      pix3[2] = B3;
   }

   return 0;
}


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

//  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 = TX("PATTERN");

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

***/

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

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

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

   zdialog_add_widget(zd,"hbox","hbs11","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labpattw","hbs11",TX("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",TX("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",TX("Overlap"),"space=5");
   zdialog_add_widget(zd,"label","labwidth","hbs12",TX("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",TX("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",TX("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",TX("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",TX("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)                                       //  dialog changes --> instant update     26.2
{
   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 = 1;                                          //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                        //  from f_open()
   if (strmatch(event,"apply")) zd->zstat = 1;                                         //  from script

   if (zd->zstat)
   {
      if (zd->zstat == 1) edit_done(0);                                                //  [OK] 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,TX("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) {
      printf("error, no pixbuf \n");                                                   //  continuation pointless
      return 1;
   }

   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) {
         zmessageACK(Mwin,"%s \n %s",pattfile,gerror->message);
         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);                                                  //  update dialog
   zdialog_stuff(zd,"patthh",patthh);
   zdialog_stuff(zd,"olapww",olapww);
   zdialog_stuff(zd,"olaphh",olaphh);
   zdialog_stuff(zd,"zoom",zoom);

   pattern_image();                                                                    //  calculate image + pattern
   CEF->Fmods = 1;                                                                     //  image modified
   CEF->Fsaved = 0;                                                                    //  not saved
   Fpaint2();                                                                          //  update window

   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;
   int64    diff, mindiff;
   uint8    *pix1, *pix2;

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

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

   sx = sy = 1;                                                                        //  best origin found
   mindiff = 99999999999;                                                              //  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;
}


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

//  convert an image into a drawing, using a crosshatch pattern (#) for shading

namespace crosshatch_names
{
   editfunc    EFcrosshatch;
   int         GZ;                                                                     //  user input, grid size
   int         GZx = 6;                                                                //  internal grid size
   int8        *GM;                                                                    //  crosshatch grid map
}


//  menu function

void m_crosshatch(GtkWidget *, ch *menu)                                               //  26.0 
{
   using namespace crosshatch_names;

   int    crosshatch_dialog_event(zdialog* zd, ch  *event);
   void * crosshatch_thread(void *);

   printf("m_crosshatch \n");

   F1_help_topic = TX("CROSSHATCH");

   EFcrosshatch.menuname = "Crosshatch";
   EFcrosshatch.menufunc = m_crosshatch;
   EFcrosshatch.threadfunc = crosshatch_thread;

   if (! edit_setup(EFcrosshatch)) return;                                             //  setup edit
   
   GM = (int8 *) zmalloc(Eww * Ehh * sizeof(int8),"crosshatch");                       //  allocate grid map

/***
             _____________________________
            |     Crosshatch Sketch       |
            |                             |
            | grid size [___]             |
            |                             |
            |    [Reset] [Apply] [OK] [X] |
            |_____________________________|

***/

   zdialog *zd = zdialog_new(TX("Crosshatch Sketch"),Mwin,TX("Reset"),TX("Apply"),"OK","X",null);
   EFcrosshatch.zd = zd;

   zdialog_add_widget(zd,"hbox","hbgz","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labgrid","hbgz",TX("grid size"));
   zdialog_add_widget(zd,"zspin","GZ","hbgz","6|99|1|6");                              //  low limit = GZx

   GZ = GZx;                                                                           //  initial grid value

   zdialog_resize(zd,400,0);
   zdialog_run(zd,crosshatch_dialog_event);                                            //  run dialog - parallel

   return;
}


//  dialog event and completion callback function

int crosshatch_dialog_event(zdialog *zd, ch  *event)
{
   using namespace crosshatch_names;
   
   int      cc, Fapply = 0;
   int      px, py, ii;
   int      Ewwx = 0, Ehhx = 0;
   PXM      *pxmx;

   if (strmatch(event,"done")) zd->zstat = 3;                                          //  apply and quit
   if (strmatch(event,"cancel")) zd->zstat = 4;                                        //  cancel
   if (strmatch(event,"apply")) Fapply = 1;                                            //  from script
   if (strmatch(event,"paint")) Fapply = 1;                                            //  mouse paint

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

   if (zd->zstat == 3)                                                                 //  [OK]
   {
      if (CEF->Fmods) edit_done(0);                                                    //  complete edit
      else edit_cancel(0);                                                             //  no change
      zfree(GM);
      return 1;
   }

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

   if (strmatch(event,"GZ")) {                                                         //  new grid size
      zdialog_fetch(zd,"GZ",GZ);
   }
   
   if (Fapply) 
   {
      edit_reset();
      
      if (GZ > GZx) {
         Ewwx = Eww;                                                                   //  save org. size
         Ehhx = Ehh;
         Eww = Eww * 1.0 * GZx / GZ;                                                   //  downsize E3
         Ehh = Ehh * 1.0 * GZx / GZ;
         PXM_free(E3pxm);
         E3pxm = PXM_rescale(E1pxm,Eww,Ehh);
      }
      
      cc = Eww * Ehh * sizeof(int8);                                                   //  clear grid map
      memset(GM,0,cc);

      for (py = 0; py < Ehh; py++)                                                     //  mark grid map pixels, '/' lines
      for (px = py - py/GZx*GZx; px < Eww; px += GZx) {                                //  0, 1, 2 ... GZ-1, 0, 1, 2 ...
         ii = py * Eww + px;
         GM[ii] = 1;
      }

      for (py = 0; py < Ehh; py++)                                                     //  mark grid map pixels, '\' lines
      for (px = GZx - (py - py/GZx*GZx); px < Eww; px += GZx) {                        //  GZ, GZ-1, GZ-2 ... 0, GZ, GZ-1, GZ-2 ...
         ii = py * Eww + px;
         GM[ii] = 1;
      }

      thread_signal();                                                                 //  update the image
      thread_wait();

      if (GZ > GZx) {
         Eww = Ewwx;                                                                   //  restore original size
         Ehh = Ehhx;
         pxmx = PXM_rescale(E3pxm,Eww,Ehh);
         PXM_free(E3pxm);
         E3pxm = pxmx;
      }
   }

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

   Fpaint2();                                                                          //  update window

   return 1;
}


//  thread function

void * crosshatch_thread(void *arg)
{
   using namespace crosshatch_names;
   
   int      px, py, qx, qy, rx, ry;
   int      ii, dist;
   float    *pix31, *pix32;
   float    R31, G31, B31, R32, G32, B32;
   float    max;

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

      if (sa_stat == sa_stat_fini) {                                                   //  select area active
         ii = py * Eww + px;
         dist = sa_pixmap[ii];                                                         //  distance from edge
         if (! dist) continue;                                                         //  outside area
      }
   
      ii = py * Eww + px;
      if (! GM[ii]) continue;                                                          //  skip unmarked pixel
      
      pix31 = PXMpix(E3pxm,px,py);                                                     //  RGB source pixel
      R31 = pix31[0];
      G31 = pix31[1];
      B31 = pix31[2];
      
      for (dist = 1; dist < GZx; dist++)                                               //  search for RGB destination pixel
      {                                                                                //    in order of increasing distance
         if (R31 + G31 + B31 < 1) break;                                               //  source pixel is empty

         for (qy = -dist; qy <= +dist; qy++)
         for (qx = -dist; qx <= +dist; qx++) 
         {
            if (R31 + G31 + B31 < 1) break;                                            //  source pixel is empty

            if (abs(qx) != dist && abs(qy) != dist) continue;
            rx = px + qx;                                                              //  pixel at distance 'dist' from px/py
            ry = py + qy;
            if (rx < 0 || rx > Eww-1) continue;                                        //  off the image edge
            if (ry < 0 || ry > Ehh-1) continue;
            ii = ry * Eww + rx;
            if (GM[ii]) continue;                                                      //  skip marked pixel
            
            pix32 = PXMpix(E3pxm,rx,ry);                                               //  potential RGB destination pixel
            R32 = pix32[0];
            G32 = pix32[1];
            B32 = pix32[2];
            if (R32 >= 255 && G32 >= 255 && B32 >= 255) continue;                      //  pixel is full           
            
            max = R31;                                                                 //  move as much RGB as possible
            if (255 - R32 < max) max = 255 - R32;                                      //   from source to destination pixel
            R31 -= max;
            R32 += max;

            max = G31;
            if (255 - G32 < max) max = 255 - G32;
            G31 -= max;
            G32 += max;

            max = B31;
            if (255 - B32 < max) max = 255 - B32;
            B31 -= max;
            B32 += max;
            
            pix31[0] = R31;                                                            //  update pixel RGB values
            pix31[1] = G31;
            pix31[2] = B31;

            pix32[0] = R32;
            pix32[1] = G32;
            pix32[2] = B32;
         }
      }
   }

   return 0;
}


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

//  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 = TX("DITHER");

   printf("m_dither \n");

   if (! curr_file) {
      zmessageACK(Mwin,TX("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(TX("Dither Image"),Mwin,"X",null);

   zdialog_add_widget(zd,"hbox","hb0","dialog");
   zdialog_add_widget(zd,"button","dither0","hb0",TX("Dither0"),"space=3");
   zdialog_add_widget(zd,"label","lab0","hb0",TX("Roy Lichtenstein effect"),"space=3");

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

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

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

   zdialog_add_widget(zd,"hbox","hb4","dialog");
   zdialog_add_widget(zd,"button","dither4","hb4",TX("Dither4"),"space=3");
   zdialog_add_widget(zd,"label","lab4","hb4",TX("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 = TX("DITHER");

   printf("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,TX("Apply"),"OK","X",null);
   EFdither0.zd = zd;

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

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

   printf("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(TX("Dither1"),Mwin,TX("Apply"),"OK","X",null);                    //  dither1 dialog
   CEF->zd = zd;

   zdialog_add_widget(zd,"hbox","hbres","dialog");
   zdialog_add_widget(zd,"label","labres","hbres",TX("Resolution"),"space=5");
   zdialog_add_widget(zd,"zspin","res","hbres","2|30|1|4","space=5|size=3");
   zdialog_add_widget(zd,"radio","RGB","dialog",TX("RGB color"));
   zdialog_add_widget(zd,"radio","B&W","dialog",TX("black/white"));
   zdialog_add_widget(zd,"check","rpos","dialog",TX("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;
   int         pskip = 0;

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

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

   return 0;                                                                           //  exit thread
}


//  worker thread function 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;
   int         pskip = 0;

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

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

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

   printf("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,TX("Apply"),"OK","X",null);                    //  dither dialog
   CEF->zd = zd;

   zdialog_add_widget(zd,"hbox","hbres","dialog");
   zdialog_add_widget(zd,"label","labres","hbres",TX("resolution"),"space=5");
   zdialog_add_widget(zd,"zspin","res","hbres","2|30|1|4","space=5|size=3");
   zdialog_add_widget(zd,"hbox","hbdep","dialog");
   zdialog_add_widget(zd,"label","labdep","hbdep",TX("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",TX("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 = TX("DITHER");

   printf("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(TX("Dither3"),Mwin,TX("Apply"),"OK","X",null);                    //  dither dialog
   CEF->zd = zd;

   zdialog_add_widget(zd,"hbox","hbres","dialog");
   zdialog_add_widget(zd,"label","labres","hbres",TX("resolution"),"space=5");
   zdialog_add_widget(zd,"zspin","res","hbres","2|30|1|4","space=5|size=3");
   zdialog_add_widget(zd,"hbox","hbpal","dialog");
   zdialog_add_widget(zd,"hbox","hbpal","dialog");
   zdialog_add_widget(zd,"label","labpal","hbpal",TX("palette:"),"space=5");
   zdialog_add_widget(zd,"zentry","palname","hbpal",TX("none"),"size=20|space=5");
   zdialog_add_widget(zd,"button","LOAD","hbpal",TX("Load"),"space=5");
   zdialog_add_widget(zd,"hbox","hbcomp","dialog");
   zdialog_add_widget(zd,"check","comp","hbcomp",TX("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);

   zdialog  *zdpop;
   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(TX("palette file"),MWIN,"file",palettes_folder,0);
      if (pp) {
         zdialog_show(zd,0);
         zdpop = zmessage_post(Mwin,"parent",0,TX("loading palette ..."));             //  25.1
         Ncolors = load_palette_map(pp);                                               //  CPU loops for a long time
         zdialog_free(zdpop);
         if (Ncolors) {
            zmessage_post(Mwin,"parent",3,TX("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;

   printf("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,TX("palette has %d colors"),Np);
      return 0;
   }

   if (Np == maxNp) {
      zmessageACK(Mwin,TX("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 = TX("DITHER");

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

   zdialog_add_widget(zd,"hbox","hbinputs","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labradius","hbinputs",TX("radius"),"space=3");
   zdialog_add_widget(zd,"zspin","radius","hbinputs","2|20|1|10","space=3|size=3");
   zdialog_add_widget(zd,"button","start","hbinputs",TX("start"),"space=5");
   zdialog_add_widget(zd,"button","stop","hbinputs",TX("stop"),"space=5");
   zdialog_add_ttip(zd,"start",TX("start blob condensation process"));
   zdialog_add_ttip(zd,"stop",TX("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
      }
   }

   else Frun = 0;

   return 1;
}


//  timer function - start thread every interval

int dither4_timerfunc(void *)
{
   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 = TX("ENGRAVE");

   printf("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(TX("Engrave"),Mwin,TX("Apply"),"OK","X",null);
   EFengrave.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labsize","hb1",TX("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",TX("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",TX("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
               pix1 = PXMpix(E1pxm,qx,qy);                                             //  input pixel                           25.4
               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;
}


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

//  add glare around bright objects

namespace addglare_names
{
   editfunc    EFaddglare;
   float       power;               //  0 - 1.0       overall add glare power
   float       limit;               //  100 - 255     pixel min. brightness for glare emission
   float       range;               //  1 - 99        glare range from emitting pixels
   
   struct lum_t 
   {
      float    R, G, B;             //  maps pixel illumination from nearby illuminating pixels
      int      count;
   };

   lum_t       *lummap;
   size_t      lumcc;
}


//  menu function

void m_addglare(GtkWidget *, ch *menu)                                                 //  26.0
{
   using namespace addglare_names;

   int    addglare_dialog_event(zdialog* zd, ch  *event);
   void * addglare_thread(void *);
   
   printf("m_addglare \n");

   F1_help_topic = TX("ADD GLARE");

   EFaddglare.menuname = "addglare";
   EFaddglare.menufunc = m_addglare;
   EFaddglare.Farea = 2;                                                               //  select area usable
   EFaddglare.Fpaintedits = 1;                                                         //  use with paint edits OK
   EFaddglare.Fscript = 1;                                                             //  scripting supported
   EFaddglare.threadfunc = addglare_thread;

   if (! edit_setup(EFaddglare)) return;                                               //  setup edit
   
   lumcc = Eww * Ehh * sizeof(lum_t);                                                  //  memory for illumination map
   lummap = (lum_t *) zmalloc(lumcc,"addglare");
   
/***
       ________________________________
      |          Add Glare             |
      |                                |
      | Power  ===========[]==== [__]  |                                               //  overall glare power, 0 - 1.0
      | Limit  ====[]=========== [__]  |                                               //  source pixel min. brightness, 200 - 255
      | Range  =========[]====== [__]  |                                               //  target pixel max. distance, 1 - 99
      |                                |
      |               [Apply] [OK] [X] |
      |________________________________|

***/

   zdialog *zd = zdialog_new(TX("Add Glare"),Mwin,TX("Apply"),"OK","X",null);
   EFaddglare.zd = zd;

   zdialog_add_widget(zd,"hbox","hbp","dialog",0,"space=5|expand");
   zdialog_add_widget(zd,"label","labp","hbp",TX("Power"));
   zdialog_add_widget(zd,"hscale2","power","hbp","0|1.0|0.01|0","expand");

   zdialog_add_widget(zd,"hbox","hbl","dialog",0,"space=5|expand");
   zdialog_add_widget(zd,"label","labp","hbl",TX("Limit"));
   zdialog_add_widget(zd,"hscale2","limit","hbl","200|255|1|250","expand");

   zdialog_add_widget(zd,"hbox","hbr","dialog",0,"space=5|expand");
   zdialog_add_widget(zd,"label","labp","hbr",TX("Range"));
   zdialog_add_widget(zd,"hscale2","range","hbr","1|99|1|20","expand");

   power = 0;
   limit = 250;
   range = 20;

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

   return;
}


//  dialog event and completion callback function

int addglare_dialog_event(zdialog *zd, ch  *event)
{
   using namespace addglare_names;
   
   int   Fapply = 0;

   if (strmatch(event,"done")) zd->zstat = 1;                                          //  apply and quit
   if (strmatch(event,"cancel")) zd->zstat = 3;                                        //  cancel
   if (strmatch(event,"apply")) Fapply = 1;                                            //  from script
   if (strmatch(event,"paint")) Fapply = 1;                                            //  mouse paint

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

   if (zd->zstat == 2)                                                                 //  [OK]
   {
      if (CEF->Fmods) edit_done(0); 
      else edit_cancel(0);                                                             //  no change
      zfree(lummap);
      return 1;
   }

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

   if (strstr("power limit range",event)) {                                            //  new user input
      zdialog_fetch(zd,"power",power);
      zdialog_fetch(zd,"limit",limit);
      zdialog_fetch(zd,"range",range);
   }

   if (Fapply) thread_signal();

   return 1;
}


//  thread function

void * addglare_thread(void *arg)
{
   using namespace addglare_names;

   void * addglare_wthread(void *);
   
   int      px, py, rx, ry, ii, pskip = 0;
   float    *pixB, *pixD, Pdist, F;

   if (Fpaintedits && ! Mdrag) return 0;                                               //  if mouse paint, must be drag
   
   memset(lummap,0,lumcc);                                                             //  clear illumination map
   
   progress_setgoal(Eww * Ehh);                                                        //  initz. progress monitor

   for (py = 0; py < Ehh; py++)                                                        //  loop all pixels
   for (px = 0; px < Eww; px++)
   {
      if (++pskip == 10) {                                                             //  every 10th pixel
         pskip = 0;                                                                    //  (reduce call overhead) 
         progress_addvalue(10);                                                        //  track progress
      }

      pixB = PXMpix(E1pxm,px,py);
      if (pixB[0] < limit && pixB[1] < limit && pixB[2] < limit) continue;             //  skip non-illuminating (dark) pixels

      for (ry = py-range; ry <= py+range; ry++)                                        //  loop pixels within range of 
      for (rx = px-range; rx <= px+range; rx++)                                        //    illuminating pixel
      {
         if (rx < 0 || rx > Eww-1) continue;                                           //  pixel off the image edge
         if (ry < 0 || ry > Ehh-1) continue;
         
         pixD = PXMpix(E1pxm,rx,ry);                                                   //  skip illuminating pixels
         if (pixD[0] >= limit || pixD[1] >= limit || pixD[2] >= limit) continue;

         Pdist = sqrtf((rx-px)*(rx-px) + (ry-py)*(ry-py));                             //  distance from source
         if (Pdist > range) continue;                                                  

         ii = ry * Eww + rx;
         F = (range - Pdist) / range;                                                  //  near 1 at Pdist=1 --> 0 at Pdist=range
         lummap[ii].R += pixB[0] * F;                                                  //  accumulate illumination of dark pixel
         lummap[ii].G += pixB[1] * F;                                                  //    from nearby illuminating pixels
         lummap[ii].B += pixB[2] * F;
         lummap[ii].count++;                                                           //  accumulate count of illuminating pixels
      }
   }
   
   get_edit_pixels_init(NSMP,0);                                                       //  initz. pixel loop
   do_wthreads(addglare_wthread,NSMP);                                                 //  run working threads

   progress_setgoal(0);

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

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


void * addglare_wthread(void *arg)                                                     //  worker thread function
{
   using namespace addglare_names;

   int         index = *((int *) arg);
   int         Fend, px, py, ii;
   float       *pix1, *pix3;
   float       blend;
   float       F, R1, G1, B1, R3, G3, B3, R9, G9, B9;

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

      if (Fescape) break;
      
      ii = py * Eww + px;
      if (lummap[ii].count == 0) continue;                                             //  pixel not illuminated

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

      R1 = R9 = pix1[0];                                                               //  input image RGB values
      G1 = G9 = pix1[1];
      B1 = B9 = pix1[2];

      R3 = pix3[0];                                                                    //  current output image RGB
      G3 = pix3[1];
      B3 = pix3[2];
      
      F = power / lummap[ii].count;
      R9 += F * lummap[ii].R;                                                          //  add illumination to pixel      
      G9 += F * lummap[ii].G;
      B9 += F * lummap[ii].B;
      
      RGBfix(R9,G9,B9);                                                                //  rescale within 255

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


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

//  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 = TX("Create Mosaic");
   ch       label[12];
   int      cc;

   F1_help_topic = TX("MOSAIC");

   printf("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",TX("Tile"),"space=3");
   zdialog_add_widget(zd,"label","labwidth","hbsize",TX("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",TX("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",TX("Tiles"),"space=3");
   zdialog_add_widget(zd,"label","labNtiles","hbcreate","0","space=5");
   zdialog_add_widget(zd,"button","image","hbcreate",TX("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",TX("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,TX("exceeded max. tiles: %d"),maxtiles);
         for (ii = 0; ii < NF; ii++)                                                   //  free memory                           25.1
            zfree(flist[ii]);
         zfree(flist);
         block = 0;
         return 1;
      }

      if (NF < 100) {                                                                  //  too few
         zmessageACK(Mwin,TX("only %d tile images found"),NF);
         for (ii = 0; ii < NF; ii++)                                                   //  free memory                           25.1
            zfree(flist[ii]);
         if (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) {
         printf("pixbuf error: %s \n",gerror->message);                                //  message includes file name
         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) {
         printf("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 = TX("ADD NOISE");

   printf("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(TX("Add Random Noise"),Mwin,TX("Reset"),TX("Apply"),"OK","X",null);
   CEF->zd = zd;

   zdialog_add_widget(zd,"hbox","hbnoise","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labsize","hbnoise",TX("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",TX("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;
   int      pskip = 0;

   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
      }

      if (++pskip == 10) {                                                             //  every 10th pixel                      26.0
         pskip = 0;                                                                    //  (reduce call overhead) 
         progress_addvalue(index,10);                                                  //  track 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)
{
   using namespace set_colors_names;

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

   F1_help_topic = TX("SET COLORS");

   printf("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(TX("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",TX("Red"),"space=3");
   zdialog_add_widget(zd,"zspin","Rcolors","hbrgb","1|256|1|6","space=3");
   zdialog_add_widget(zd,"label","Glab","hbrgb",TX("Green"),"space=3");
   zdialog_add_widget(zd,"zspin","Gcolors","hbrgb","1|256|1|6","space=3");
   zdialog_add_widget(zd,"label","Blab","hbrgb",TX("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",TX("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 = TX("SHIFT COLORS");

   printf("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(TX("Shift Colors"),Mwin,TX("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",TX("Red"));
   zdialog_add_widget(zd,"label","labr","vb1",TX("Green"));
   zdialog_add_widget(zd,"label","labr","vb1",TX("Blue"));
   zdialog_add_widget(zd,"label","labr","vb1",TX("All"));

   zdialog_add_widget(zd,"label","labg","vb2",TX("Green"));
   zdialog_add_widget(zd,"label","labb","vb2",TX("Blue"));
   zdialog_add_widget(zd,"label","labr","vb2",TX("Red"));
   zdialog_add_widget(zd,"label","laba","vb2",TX("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",TX("Blue"));
   zdialog_add_widget(zd,"label","labr","vb4",TX("Red"));
   zdialog_add_widget(zd,"label","labg","vb4",TX("Green"));
   zdialog_add_widget(zd,"label","laba","vb4",TX("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 = TX("ALIEN COLORS");

   printf("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] |
         |__________________________|

***/

   zdialog *zd = zdialog_new(TX("Alien Colors"),Mwin,TX("Apply"),"OK","X",null);
   CEF->zd = zd;
   zdialog_add_widget(zd,"hbox","hbbsz","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labbsz","hbbsz",TX("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",TX("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]
         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 = TX("CUSTOM KERNEL");

   printf("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
   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(TX("Custom Kernel"),Mwin,TX("Reset"),TX("Apply"),"OK","X",null);
   EFanykernel.zd = zd;

   zdialog_add_widget(zd,"hbox","hbkern","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labkern","hbkern",TX("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",TX("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",TX("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",TX("Data file"),"space=3");
   zdialog_add_widget(zd,"button","load","hbfile",TX("Load"),"space=3");
   zdialog_add_widget(zd,"button","save","hbfile",TX("Save"),"space=3");

   zdialog_add_widget(zd,"hbox","hbfname","dialog",0,0);
   zdialog_add_widget(zd,"label","file","hbfname",TX("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       *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
   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(TX("Load settings from file"),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,TX("file format error: %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,TX("file format error: %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

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


