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

   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 - Edit menu functions

   get_edit_pixels            get pixels from image, area, mouse
   m_rotate                   rotate or level an image
   m_upright                  upright a rotated image
   m_crop                     crop image margins
   m_retouch                  brightness, contrast, color ...
   blackbodyRGB               convert deg. K to RGB factors
   m_rescale                  rescale image
   m_split_screen1            edit function before/after using split-screen
   m_split_screen2            view 2-image difference using split-screen
   m_margins                  add margins around image
   m_markup                   draw on image image: text, line, box, oval
   m_draw_text                draw text on image
   m_draw_line                draw line/arrow on image
   m_draw_box                 draw box on image
   m_draw_oval                draw oval on image
   m_color_mode               convert: B&W, color, sepia, positive, negative
   m_paint_edits              set up edit via mouse painting
   m_paint_image              paint on the image with a color
   RGB_chooser                select RGB color or use color chooser file
   HSL_chooser                select HSL color
   HSLtoRGB                   convert HSL color to RGB
   RGBtoHSL                   convert RGB color to HSL
   m_paint_transp             paint more or less transparency on an image
   m_area_fill                fill selected areas or transparent areas
   m_copy_in_image            copy pixels within one image
   m_copy_from_image          copy pixels from source image to target image
   m_copy_prior_edit          copy pixels from prior edits in current image
   m_template                 template for new edit functions

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

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


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

   get_edit_pixels() function

   Feed pixels to edit function, one at a time
   modes of operation:
      Fpaintedits = 0, sa_stat != sa_stat_fini
         return all pixels with blend = 1
      sa_stat == sa_stat_fini
         return all pixels within selected area
      Fpaintedits = 1
         return all pixela within mouse radius Mradius
         blend calculation for pixel at Pradius from mouse position:
            blend = MCpower + (MEpower - MCpower) * Pradius / Mradius
            where MCpower/MEpower is edit power at center/edge of mouse circle
            and Pradius/Mradius = pixel/circle radius

   initial call:
      get_edit_pixels_init(Nthreads,margin)

   subsequent calls (pixel processing loop)
      stat = get_edit_pixels(int thread, int &px, int &py, float &blend)
      stat = 0  next pixel is returned
             1  finished, no more pixels

****/

namespace get_edit_pixels_names
{
   int      Nt;
   int      FTF[XSMP];
   int      pxlo, pxhi, pylo, pyhi;
   float    Cpow, Epow;
}


//  initialization for returning pixels to the edit threads

void get_edit_pixels_init(int Nthreads, int margin)
{
   using namespace get_edit_pixels_names;

   int      cc;

   if (! CEF) zappcrash("no edit function active");
   if (! E3pxm) zappcrash("no E3pxm");

   cc = NSMP * sizeof(int);                                                            //  set first time flags for all threads
   memset(FTF,1,cc);

   if (! Nthreads) return;                                                             //  re-initz. same area

   Nt = Nthreads;                                                                      //  edit thread count

   if (Fpaintedits)                                                                    //  paint_edit() is active,
   {                                                                                   //    process pixels in mouse circle
      Cpow = 0.01 * MCpower;
      Epow = 0.01 * MEpower;

      pxlo = Mxposn - Mradius;
      if (pxlo < 0) pxlo = 0;
      pxlo += margin;
      pxhi = Mxposn + Mradius;
      if (pxhi > Eww) pxhi = Eww;
      pxhi -= margin;

      pylo = Myposn - Mradius;
      if (pylo < 0) pylo = 0;
      pylo += margin;
      pyhi = Myposn + Mradius;
      if (pyhi > Ehh) pyhi = Ehh;
      pyhi -= margin;
      return;
   }

   if (sa_stat == sa_stat_fini)                                                        //  select area is valid and enabled,
   {                                                                                   //    process pixels within area
      pxlo = sa_minx + margin;
      pxhi = sa_maxx - margin;
      pylo = sa_miny + margin;
      pyhi = sa_maxy - margin;
      return;
   }

   pxlo = pylo = margin;                                                               //  process entire image
   pxhi = Eww - margin;
   pyhi = Ehh - margin;

   editpixels_pxlo = pxlo;                                                             //  initz. externals
   editpixels_pxhi = pxhi;
   editpixels_pylo = pylo;
   editpixels_pyhi = pyhi;

   return;
}


//  return next pixel for edit mode: full image, area, mouse paint

int get_edit_pixels(int thread, int &px, int &py, float &blend)
{
   using namespace get_edit_pixels_names;

   int            ii, dist;
   float          Pradius;

   if (Fpaintedits)                                                                    //  edit within mouse circle
   {
      if (FTF[thread])
      {
         FTF[thread] = 0;
         px = pxlo;                                                                    //  initial px/py
         py = pylo + thread;
         if (py >= pyhi) return 1;                                                     //  fini
      }

      while (true)
      {
         px++;                                                                         //  next px/py
         if (px >= pxhi) {
            px = pxlo;
            py += Nt;
            if (py >= pyhi) return 1;                                                  //  end return
         }

         if (sa_stat == sa_stat_fini) {                                                //  select area active
            ii = Eww * py + px;
            dist = sa_pixmap[ii];
            if (! dist) continue;                                                      //  ignore pixels outside area
         }

         Pradius =  (px - Mxposn) * (px - Mxposn);                                     //  get (px,py) discance from mouse center
         Pradius += (py - Myposn) * (py - Myposn);
         Pradius = sqrtf(Pradius);
         if (Pradius < Mradius) break;                                                 //  pixel within mouse radius, keep
      }

      blend = Cpow + (Epow - Cpow) * Pradius / Mradius;                                //  Cpow to Epow for center to edge pixel
      blend = blend * blend;                                                           //  quadratic scale
      if (Mbutton > 2) blend = -blend;                                                 //  left/right button == paint/erase edit

      return 0;
   }

   if (sa_stat == sa_stat_fini)                                                        //  edit within select area
   {
      if (FTF[thread])
      {
         FTF[thread] = 0;
         px = pxlo;                                                                    //  initial px/py
         py = pylo + thread;
         if (py >= pyhi) return 1;                                                     //  fini
      }

      while (true)
      {
         px++;                                                                         //  next px/py
         if (px >= pxhi) {
            px = pxlo;
            py += Nt;
            if (py >= pyhi) return 1;                                                  //  fini
         }

         ii = Eww * py + px;                                                           //  blend = 0/1 for pixel out/in area
         dist = sa_pixmap[ii];
         if (! dist) continue;                                                         //  ignore pixels outside area
         blend = 1;
         return 0;
      }
   }

   blend = 1;                                                                          //  full image edit

   if (FTF[thread])
   {
      FTF[thread] = 0;
      px = pxlo;                                                                       //  initial px/py
      py = pylo + thread;
      if (py < pyhi) return 0;
      return 1;                                                                        //  fini
   }

   px++;                                                                               //  next px/py
   if (px < pxhi) return 0;
   px = pxlo;
   py += Nt;
   if (py < pyhi) return 0;
   return 1;                                                                           //  fini
}


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

//  rotate or level an image

namespace rotate_names
{
   editfunc EFrotateimage;
   zdialog  *zd;

   double   rotate_goal, rotate_angle;                                                 //  target and actual rotation
   int      mirror;                                                                    //  mirror: horizontal or vertical
   int      Ftimer1;
   double   Ftimer2;

   int   rotate_watchdog(void *);
   int   dialog_event(zdialog *zd, ch *event);
   void  mousefunc();
   void  KBfunc(int key);
   void  rotate_final();
   void  rotate_func(int fast);
   void  drawlines(cairo_t *cr);
}


//  menu function

void m_rotate(GtkWidget *, ch *menu)
{
   using namespace rotate_names;

   ch          *rotate_message = TX("Drag on image to level image.\n"
                                 "Click on image for level lines.");
   F1_help_topic = TX("ROTATE");

   printf("m_rotate \n");

   EFrotateimage.menufunc = m_rotate;                                                  //  menu function
   EFrotateimage.menuname = "Rotate";
   EFrotateimage.mousefunc = mousefunc;
   EFrotateimage.FprevReq = 1;                                                         //  use preview (small) size

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

   PXM_addalpha(E0pxm);
   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

   rotate_goal = rotate_angle = 0;                                                     //  initially no rotation

/***
             ______________________________________
            |                                      |
            |   Drag on image to level image.      |
            |   Click on image for level lines.    |
            |                                      |
            |  Rotate: [-90°] [+90°] [180°] [ 0 ]  |
            |  Mirror: [] horizontal  [] vertical  |
            |    [Auto Level]  [Auto Upright]      |
            |                                      |
            |                      [Crop] [OK] [X] |
            |______________________________________|

***/

   zd = zdialog_new(TX("Rotate Image"),Mwin,TX("Crop"),"OK","X",null);
   EFrotateimage.zd = zd;

   zdialog_add_widget(zd,"label","labrotate","dialog",rotate_message,"space=3");

   zdialog_add_widget(zd,"hbox","hbrot","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labrotate","hbrot",TX("Rotate:"),"space=5");
   zdialog_add_widget(zd,"imagebutt","-90","hbrot","rotate-left.png","size=32|space=5");
   zdialog_add_widget(zd,"imagebutt","+90","hbrot","rotate-right.png","size=32|space=5");
   zdialog_add_widget(zd,"button","180","hbrot","180","space=5");
   zdialog_add_widget(zd,"zspin","degrees","hbrot","-180|180|0.1|0","space=5");

   zdialog_add_widget(zd,"hbox","hbmir","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labmir","hbmir",TX("Mirror:"),"space=5");
   zdialog_add_widget(zd,"zbutton","horz","hbmir",TX("horizontal"),"space=5");
   zdialog_add_widget(zd,"zbutton","vert","hbmir",TX("vertical"),"space=5");

   zdialog_add_widget(zd,"hbox","hbauto","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","level","hbauto",TX("Auto Level"),"space=10");
   zdialog_add_widget(zd,"button","upright","hbauto",TX("Auto Upright"),"space=8");

   zdialog_add_ttip(zd,"level",TX("level based on EXIF roll angle"));
   zdialog_add_ttip(zd,"upright",TX("upright based on EXIF orientation"));

   takeMouse(mousefunc,dragcursor);                                                    //  connect mouse function
   Fzoom = 0;

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

   g_timeout_add(100,rotate_watchdog,0);                                               //  start rotate watchdog
   Ftimer1 = 1;                                                                        //  rotate() active
   Ftimer2 = 0;                                                                        //  rotate fix pending

   return;
}


//  watchdog watchdog function - watch for fast low-quality rotates
//  and fix them when rotation has apparently ended.

int rotate_names::rotate_watchdog(void *)
{
   if (! Ftimer1) return 0;                                                            //  trim_rotate() ended
   if (! Ftimer2) return 1;                                                            //  nothing pending
   if (get_seconds() - Ftimer2 < 1.0) return 1;                                        //  < 1 sec. since last rotate
   Ftimer2 = 0;                                                                        //  reset pending flag
   rotate_angle = 0;
   rotate_func(0);                                                                     //  high quality rotate
   return 1;
}


//  dialog event and completion callback function

int rotate_names::dialog_event(zdialog *zd, ch *event)
{
   using namespace rotate_names;

   int         angle = 0, Fcrop = 0;
   double      fangle;
   ch          *metatag[1];
   ch          *ppv[1];

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

   if (strmatch(event,"undo"))                                                         //  'undo' button was used
   {
      Ftimer1 = Ftimer2 = 0;                                                           //  stop watchdog timer
      Ntoplines = 0;                                                                   //  remove leveling lines
      edit_reset();
      zdialog_stuff(zd,"degrees",0);
      rotate_goal = rotate_angle = 0;
      return 1;
   }

   if (strmatch(event,"focus")) {
      takeMouse(mousefunc,dragcursor);                                                 //  re-connect mouse function
      return 1;
   }

   if (zd->zstat)                                                                      //  dialog complete
   {
      Ftimer1 = Ftimer2 = 0;                                                           //  stop watchdog timer
      Ntoplines = 0;                                                                   //  remove leveling lines

      if (zd->zstat == 1 || zd->zstat == 2)                                            //  [crop] or [OK]
      {
         if (zd->zstat == 1) Fcrop = 1;                                                //  remember if crop

         if (rotate_goal == 0) {                                                       //  no rotate was done
            edit_cancel(0);
            if (Fcrop) m_crop(0,0);                                                    //  [crop] was pressed
            return 1;
         }

         edit_fullsize();                                                              //  get full size image

         rotate_angle = 0;                                                             //  reset to no rotation
         rotate_func(0);                                                               //  final rotate, high quality
         
         if (! E3pxm) {
            edit_cancel(0);                                                            //  poss. image too big                   26.3
            return 1;
         }

         fangle = fabs(rotate_angle);
         if (fangle != 90 && fangle != 180) {                                          //  small auto sharpen if not 90/180
            sharp_AC(30,20);
            printf("small auto sharpen done \n");
         }

         Fupright = Flevel = 1;                                                        //  mark uprighted (for metadata update)

         edit_addhist("angle:%.1f",rotate_angle);                                      //  edit parms > edit hist
         CEF->Fmods++;                                                                 //  finish edit
         edit_done(0);

         if (Fcrop) m_crop(0,"auto");                                                  //  [crop] was pressed

         return 1;
      }

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

   if (zstrstr("+90 -90 180",event))                                                   //  [-90] [+90] [180] button
   {
      if (strmatch(event,"+90"))                                                       //  90 deg. CW
         rotate_goal += 90;

      if (strmatch(event,"-90"))                                                       //  90 deg. CCW
         rotate_goal -= 90;

      if (strmatch(event,"180"))
         rotate_goal += 180;

      if (rotate_goal > 180) rotate_goal -= 360;                                       //  keep within -180 to +180 deg.
      if (rotate_goal < -180) rotate_goal += 360;
      if (fabs(rotate_goal) < 0.01) rotate_goal = 0;                                   //  = 0 within my precision
      zdialog_stuff(zd,"degrees",rotate_goal);

      rotate_func(0);                                                                  //  high quality rotate
      Fupright = Flevel = 1;                                                           //  mark uprighted (for metadata update)
   }

   if (strmatch(event,"degrees"))                                                      //  manual input degrees
   {
      zdialog_fetch(zd,"degrees",rotate_goal);

      if (rotate_goal > 180) rotate_goal -= 360;                                       //  keep within -180 to +180 deg.
      if (rotate_goal < -180) rotate_goal += 360;
      if (fabs(rotate_goal) < 0.01) rotate_goal = 0;                                   //  = 0 within my precision
      zdialog_stuff(zd,"degrees",rotate_goal);

      rotate_func(1);                                                                  //  fast low quality rotate
      Fupright = Flevel = 1;                                                           //  mark uprighted (for metadata update)
   }

   if (strmatch(event,"level"))                                                        //  auto level using EXIF RollAngle
   {
      metatag[0] = (ch *) meta_rollangle_tag;
      meta_get(curr_file,metatag,ppv,1);
      if (! ppv[0]) {
         zmessageACK(Mwin,TX("no metadata level data"));
         return 1;
      }
      rotate_goal = atofz(ppv[0]);
      zfree(ppv[0]);

      rotate_func(0);                                                                  //  high quality rotate
      Fupright = Flevel = 1;                                                           //  mark uprighted (for metadata update)
   }

   if (strstr("horz vert",event))                                                      //  horizontal or vertical mirror
   {
      if (strmatch(event,"horz")) mirror = 1;
      if (strmatch(event,"vert")) mirror = 2;

      PXM_free(E3pxm);
      E3pxm = PXM_upright(E0pxm,0,mirror);                                             //  mirror image
      Eww = E3pxm->ww;
      Ehh = E3pxm->hh;

      Fupright = Flevel = 1;                                                           //  mark uprighted (for metadata update)

      CEF->Fmods++;
      Fpaintnow();
      edit_done(0);                                                                    //  do not restart m_rotate()
      return 1;                                                                        //  bugfix 26.0
   }

   if (strmatch(event,"upright"))                                                      //  auto upright button
   {
      metatag[0] = (ch *) meta_orientation_tag;
      meta_get(curr_file,metatag,ppv,1);                                               //  get metadata Orientation
      if (! ppv[0] || *ppv[0] <= '1') {
         zmessageACK(Mwin,TX("no EXIF orientation data"));
         return 1;
      }

      if (*ppv[0] == '2') { angle = 0; mirror = 1; }                                   //  horizontal mirror
      if (*ppv[0] == '3') { angle = 180; mirror = 0; }                                 //  rotate 180
      if (*ppv[0] == '4') { angle = 0; mirror = 2; }                                   //  vertical mirror
      if (*ppv[0] == '5') { angle = 90; mirror = 1; }                                  //  rotate 90 + horizontal mirror
      if (*ppv[0] == '6') { angle = 90; mirror = 0; }                                  //  rotate 90
      if (*ppv[0] == '7') { angle = 270; mirror = 1; }                                 //  rotate 270 + horizontal mirror
      if (*ppv[0] == '8') { angle = 270; mirror = 0; }                                 //  rotate 270

      zfree(ppv[0]);

      PXM_free(E3pxm);
      E3pxm = PXM_upright(E0pxm,angle,mirror);                                         //  do rotate and mirro
      Eww = E3pxm->ww;
      Ehh = E3pxm->hh;

      Fupright = Flevel = 1;                                                           //  mark uprighted (for metadata update)

      CEF->Fmods++;
      Fpaintnow();
      edit_done(0);                                                                    //  do not restart m_rotate()
      return 1;                                                                        //  
   }

   return 1;
}


//  rotate image mouse function

void rotate_names::mousefunc()
{
   using namespace rotate_names;

   int      mpx, mpy, xdrag, ydrag;
   int      cx;
   zdialog  *zd = EFrotateimage.zd;

   mpx = mpy = xdrag = ydrag = 0;                                                      //  avoid gcc warnings

   if (LMclick)                                                                        //  add vertical and horizontal
   {                                                                                   //    lines at click position
      LMclick = 0;
      Ntoplines = 2;
      toplines[0].x1 = Mxclick;
      toplines[0].y1 = 0;
      toplines[0].x2 = Mxclick;
      toplines[0].y2 = Ehh-1;
      toplines[0].type = 3;                                                            //  black/white pair
      toplines[1].x1 = 0;
      toplines[1].y1 = Myclick;
      toplines[1].x2 = Eww-1;
      toplines[1].y2 = Myclick;
      toplines[1].type = 3;
      Fpaint2();
      return;
   }

   if (RMclick)                                                                        //  remove the lines
   {
      RMclick = 0;
      Ntoplines = 0;
      Fpaint2();
      return;
   }

   if (Mxdrag || Mydrag)                                                               //  drag underway
   {
      mpx = Mxdrag;
      mpy = Mydrag;
      xdrag = Mxdrag - Mxdown;
      ydrag = Mydrag - Mydown;
      Mxdown = Mxdrag;                                                                 //  reset drag origin
      Mydown = Mydrag;
      Mxdrag = Mydrag = 0;
   }

   cx = Eww / 2;                                                                       //  image center

   if (mpx > cx) rotate_goal += 30.0 * ydrag / Eww;
   else rotate_goal -= 30.0 * ydrag / Eww;

   if (rotate_goal > 180) rotate_goal -= 360;                                          //  keep within -180 to +180 deg.
   if (rotate_goal < -180) rotate_goal += 360;
   if (fabs(rotate_goal) < 0.01) rotate_goal = 0;                                      //  = 0 within my precision
   zdialog_stuff(zd,"degrees",rotate_goal);                                            //  update dialog

   rotate_func(1);                                                                     //  fast low quality rotate

   return;
}


//  rotate function
//  E3 = E1 with rotation
//  fast: 0 = slow / high quality
//        1 = fast / low quality

void rotate_names::rotate_func(int fast)
{
   using namespace rotate_names;

   zdialog     *zd = EFrotateimage.zd;
   static int  Fbusy = 0;

   if (rotate_goal == rotate_angle) return;

   if (Fbusy) return;                                                                  //  stop re-entry (mouse event)
   Fbusy = 1;

   if (fabs(rotate_goal) < 0.01) {                                                     //  insignificant rotation
      zdialog_stuff(zd,"degrees",0.0);
      PXM_free(E3pxm);                                                                 //  E1 >> E3
      E3pxm = PXM_copy(E1pxm);
      rotate_goal = 0.0;
   }
   else {
      PXM_free(E3pxm);
      E3pxm = PXM_rotate(E1pxm,rotate_goal,fast);                                      //  rotate (fast or high quality)
      if (! E3pxm) return;                                                             //  26.3
   }

   Eww = E3pxm->ww;
   Ehh = E3pxm->hh;
   rotate_angle = rotate_goal;

   if (fast) Ftimer2 = get_seconds();                                                  //  flag watchdog timer to come back
   else Ftimer2 = 0;

   CEF->Fmods++;
   Fpaintnow();

   Fbusy = 0;
   return;
}


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

//  auto upright a rotated image
//  menu function - not an edit function

void m_upright(GtkWidget *, ch *menu)
{
   ch    *metatag[1] = { meta_orientation_tag };
   ch    orient, *ppv[1];
   ch    *pp, *newfile, *delfile;
   int   err, angle, mirror;

   F1_help_topic = TX("UPRIGHT");

   printf("m_upright \n");

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

   meta_get(curr_file,(ch **) metatag,ppv,1);                                          //  get metadata Orientation
   if (! ppv[0]) {
      zmessageACK(Mwin,TX("no EXIF orientation data"));
      return;
   }

   orient = *ppv[0];
   zfree(ppv[0]);
   if (orient < '1' || orient > '8') {
      zmessageACK(Mwin,TX("invalid EXIF orientation - use Rotate function"));
      return;
   }

   angle = mirror = 0;

   if (orient == '1') { angle = 0; mirror = 0; }                                       //  do nothing
   if (orient == '2') { angle = 0; mirror = 1; }                                       //  horizontal mirror
   if (orient == '3') { angle = 180; mirror = 0; }                                     //  rotate 180
   if (orient == '4') { angle = 0; mirror = 2; }                                       //  vertical mirror
   if (orient == '5') { angle = 90; mirror = 1; }                                      //  rotate 90 + horizontal mirror
   if (orient == '6') { angle = 90; mirror = 0; }                                      //  rotate 90
   if (orient == '7') { angle = 270; mirror = 1; }                                     //  rotate 270 + horizontal mirror
   if (orient == '8') { angle = 270; mirror = 0; }                                     //  rotate 270

   err = f_open(curr_file);
   if (err) return;

   E0pxm = PXM_load(curr_file,1);                                                      //  load poss. 16-bit image
   if (! E0pxm) return;

   E3pxm = PXM_upright(E0pxm,angle,mirror);                                            //  do rotate/mirror

   PXM_free(E0pxm);
   E0pxm = E3pxm;
   E3pxm = 0;

   if (strcasestr("jp2 heic avif webp",curr_file_type)) {                              //  save these types as .jpg
      newfile = zstrdup(curr_file,"batch_upright",16);
      delfile = zstrdup(curr_file,"batch_upright");
      pp = strrchr(newfile,'.');
      strcpy(pp,"-upright.jpg");
      Fupright = 1;                                                                    //  mark uprighted (for metadata update)
      f_save(newfile,"jpg",8,0,1);                                                     //  make .jpg duplicate
      f_open(newfile);                                                                 //  show uprighted file
      remove(delfile);
      zfree(newfile);
      zfree(delfile);
   }
   else {
      Fupright = 1;                                                                    //  mark uprighted (for metadata update)
      f_save(curr_file,curr_file_type,curr_file_bpc,0,1);                              //  replace file
      f_open(curr_file);                                                               //  force full open
   }

   gallery(curr_file,"init",-1);

   return;
}


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

//  crop an image (trim margins)
//
//  fotocx.h                                                                           //  fotocx.h definitions
//    int      cropx1, cropy1, cropx2, cropy2;                                         //  crop rectangle
//    ch       *cropsizes[*];                                                          //  crop size buttons, "2000x1000" etc.
//    ch       *cropratios[*];                                                         //  crop ratio buttons, "2:1" etc.

namespace crop_names
{
   editfunc EFcrop;
   zdialog  *zd;

   int      pcropx1, pcropy1, pcropx2, pcropy2;                                        //  prior crop rectangle
   int      cropww, crophh;                                                            //  crop rectangle width and height
   int      Rlock;                                                                     //  lock width/height ratio
   int      E0ww, E0hh;                                                                //  full size image
   int      Fcorner, Fside, Fcenter;
   int      KBcorner, KBside;
   double   cropR;                                                                     //  crop ratio, width/height
   double   Zratio;                                                                    //  full / preview size   >= 1.0

   int   dialog_event(zdialog *zd, ch *event);
   void  prevcrop_menu_event(GtkWidget *, ch *menu);
   void  update_prev_cropsize();
   void  mousefunc();
   void  KBfunc(int key);
   void  crop_limits();
   void  crop_darkmargins();
   void  crop_final();
   void  drawlines(cairo_t *cr);
   void  autocrop_max();
   void  crop_customize();
}


//  menu function

void m_crop(GtkWidget *, ch *menu)                                                     //  more accurate control of crop size
{
   using namespace crop_names;

   int         ii, xmargin, ymargin;
   ch          *crop_message = TX("Drag margin box middle or edges");
   ch          text[20];

   F1_help_topic = TX("CROP");

   printf("m_crop \n");

   EFcrop.menufunc = m_crop;                                                           //  menu function
   EFcrop.menuname = "Crop";
   EFcrop.mousefunc = mousefunc;
   EFcrop.FprevReq = 1;                                                                //  use preview (small) size

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

   PXM_addalpha(E0pxm);
   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

   E0ww = E0pxm->ww;                                                                   //  full-size image dimensions
   E0hh = E0pxm->hh;                                                                   //  (preview size = Eww/Ehh)

   Zratio = 1.0 * (E0ww + E0hh) / (Eww + Ehh);                                         //  full / preview size  >= 1.0

   if (E9pxm) PXM_free(E9pxm);                                                         //  E9 invalid
   E9pxm = 0;

   cropww = 0.8 * E0ww;                                                                //  initial crop rectangle, 80% image size
   crophh = 0.8 * E0hh;
   xmargin = 0.5 * (E0ww - cropww);                                                    //  set balanced margins
   ymargin = 0.5 * (E0hh - crophh);
   cropx1 = xmargin;
   cropx2 = E0ww - xmargin;
   cropy1 = ymargin;
   cropy2 = E0hh - ymargin;
   cropR = 1.0 * cropww / crophh;                                                      //  crop ratio = width/height

   pcropx1 = 0;                                                                        //  set prior crop rectangle
   pcropy1 = 0;                                                                        //    = 100% of image
   pcropx2 = E0ww;
   pcropy2 = E0hh;

   Rlock = 0;

/***
             ____________________________________________
            |                                            |
            |      Drag margin box middle or edges       |
            |                                            |
            |  Auto crop: [Max]  [Prev]                  |
            |  Set Width: [____]  Height: [____]         |
            |  W/H Ratio: 1.778   [x] Lock               |
            |  Set Ratio: [5:4] [4:3] [8:5] [16:9] [2:1] |
            |                [Invert]  [Customize]       |
            |                                            |
            |                          [Rotate] [OK] [X] |
            |____________________________________________|

***/

   zd = zdialog_new(TX("Crop Image"),Mwin,TX("Rotate"),"OK","X",null);
   EFcrop.zd = zd;

   zdialog_add_widget(zd,"label","labcrop","dialog",crop_message,"space=5");

   zdialog_add_widget(zd,"hbox","hbcrop","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labauto","hbcrop",TX("Auto crop:"),"space=5");
   zdialog_add_widget(zd,"button","max","hbcrop",TX("Max"),"space=8");
   zdialog_add_widget(zd,"button","prev","hbcrop",TX("Prev"),"space=8");

   zdialog_add_widget(zd,"hbox","hbwh","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labW","hbwh",TX("Set Width:"),"space=5");
   zdialog_add_widget(zd,"zspin","width","hbwh","20|30000|1|1000");                    //  fotocx.h limits
   zdialog_add_widget(zd,"label","space","hbwh",0,"space=5");
   zdialog_add_widget(zd,"label","labH","hbwh",TX("Height:"),"space=5");
   zdialog_add_widget(zd,"zspin","height","hbwh","20|30000|1|600");

   zdialog_add_widget(zd,"hbox","hbrat","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labR","hbrat",TX("W/H Ratio:"),"space=5");
   zdialog_add_widget(zd,"label","cropR","hbrat","1.667");
   zdialog_add_widget(zd,"check","Rlock","hbrat","Lock","space=15");

   zdialog_add_widget(zd,"hbox","hbsr1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labsr1","hbsr1",TX("Set Ratio:"),"space=5");
   for (ii = 0; ii < cropmem; ii++)
      zdialog_add_widget(zd,"button",cropratios[ii],"hbsr1",cropratios[ii],"space=5");

   zdialog_add_widget(zd,"hbox","hbsr2","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","space","hbsr2",0,"space=50");
   zdialog_add_widget(zd,"button","invert","hbsr2",TX("Invert"),"space=5");
   zdialog_add_widget(zd,"button","custom","hbsr2",TX("Customize"),"space=5");

   zdialog_add_ttip(zd,"max",TX("set maximum crop"));
   zdialog_add_ttip(zd,"prev",TX("use previous input"));
   zdialog_add_ttip(zd,"Rlock",TX("lock width/height ratio"));

   zd = EFcrop.zd;

   zdialog_load_inputs(zd);                                                            //  restore all prior inputs

   zdialog_stuff(zd,"width",cropww);                                                   //  stuff width, height, ratio as
   zdialog_stuff(zd,"height",crophh);                                                  //    pre-calculated for this image
   snprintf(text,20,"%.3f  ",cropR);
   zdialog_stuff(zd,"cropR",text);
   zdialog_stuff(zd,"Rlock",0);                                                        //  no lock ratio

   takeMouse(mousefunc,dragcursor);                                                    //  connect mouse function

   Fzoom = 0;
   crop_darkmargins();

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

   if (menu && strmatch(menu,"auto"))                                                  //  crop after rotate
      zdialog_send_event(zd,"max");

   return;
}


//  dialog event and completion callback function

int crop_names::dialog_event(zdialog *zd, ch *event)
{
   using namespace crop_names;

   static GtkWidget  *popmenu = 0;

   static int  flip = 0;
   int         ii, Frotate = 0;
   int         width, height, delta;
   double      r1, r2, Fratio = 0;
   ch          text[20];
   ch          *pp;

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

   if (strmatch(event,"focus")) {
      takeMouse(mousefunc,dragcursor);                                                 //  re-connect mouse function
      return 1;
   }

   if (zd->zstat)                                                                      //  dialog complete
   {
      if (zd->zstat == 1 || zd->zstat == 2)                                            //  [rotate] or [OK]
      {
         if (zd->zstat == 1) Frotate = 1;                                              //  remember if [rotate]

         draw_toplines(2,0);                                                           //  erase crop rectangle
         erase_topcircles();                                                           //  erase center circle

         edit_fullsize();                                                              //  get full size image (Eww/Ehh)

         Zratio = 1.0;

         crop_final();                                                                 //  rotate/crop final image

         update_prev_cropsize();                                                       //  update prev. crop size memory

         edit_addhist("width:%d height:%d",cropww,crophh);                             //  edit parms > edit hist
         CEF->Fmods++;                                                                 //  finish edit
         edit_done(0);

         if (Frotate) m_rotate(0,0);                                                   //  start rotate function
         return 1;
      }

      draw_toplines(2,0);                                                              //  cancel - erase crop rectangle
      erase_topcircles();                                                              //  erase center circle
      edit_cancel(0);
      return 1;
   }

   zdialog_fetch(zd,"width",width);                                                    //  full image scale, E0
   zdialog_fetch(zd,"height",height);
   zdialog_fetch(zd,"Rlock",Rlock);                                                    //  get w/h ratio lock, on/off

   Fcorner = Fside = KBcorner = KBside = 0;                                            //  no mouse or KB action underway

   if (strmatch(event,"max"))                                                          //  auto crop, max within margins
   {
      zdialog_stuff(zd,"Rlock",0);                                                     //  set no ratio lock
      Rlock = 0;

      autocrop_max();                                                                  //  do max auto-crop

      zdialog_stuff(zd,"width",cropww);                                                //  update dialog values
      zdialog_stuff(zd,"height",crophh);

      cropR = 1.0 * cropww / crophh;                                                   //  new w/h ratio
      snprintf(text,20,"%.3f  ",cropR);
      zdialog_stuff(zd,"cropR",text);

      crop_darkmargins();                                                              //  show crop area in image
      return 1;
   }

   if (strmatch(event,"prev"))                                                         //  [Prev] button - get prev. crop size
   {
      if (popmenu) gtk_widget_destroy(popmenu);
      popmenu = create_popmenu();                                                      //  make popup menu with crop sizes
      for (int ii = 0; ii < cropmem; ii++)                                             //  depth = fotocx.h 'cropmem'
         if (cropsizes[ii])
            add_popmenu(popmenu,cropsizes[ii],prevcrop_menu_event,0,0);
      popup_menu(Mwin,popmenu);
      return 1;
   }

   if (zstrstr("width height",event))                                                  //  width or height input
   {
      if (width < 20) width = 20;
      if (width > E0ww) width = E0ww;
      if (height < 20) height = 20;
      if (height > E0hh) height = E0hh;

      if (Rlock)                                                                       //  w/h ratio lock
      {
         if (strmatch(event,"width"))
            height = width / cropR + 0.5;
         if (strmatch(event,"height"))
            width = height * cropR + 0.5;
         if (width > E0ww) {
            width = E0ww;
            height = width / cropR + 0.5;
         }
         if (height > E0hh) {
            height = E0hh;
            width = height * cropR + 0.5;
         }
         if (width > E0ww) width = E0ww;                                               //  rounding could put it over limit
         if (height > E0hh) height = E0hh;
      }

      zdialog_stuff(zd,"width",width);                                                 //  adjusted for ratio lock
      zdialog_stuff(zd,"height",height);

      flip = 1 - flip;                                                                 //  alternates 0, 1, 0, 1 ...

      delta = width - cropww;                                                          //  change in width

      if (delta > 0) {                                                                 //  increased width
         cropx1 = cropx1 - delta / 2;                                                  //  left and right sides equally
         cropx2 = cropx2 + delta / 2;
         if (delta % 2) {                                                              //  if increase is odd
            cropx1 = cropx1 - flip;                                                    //    add 1 alternatively to each side
            cropx2 = cropx2 + 1 - flip;
         }
         if (cropx1 < 0) {                                                             //  add balance to upper limit
            cropx2 = cropx2 - cropx1;
            cropx1 = 0;
         }
         if (cropx2 > E0ww) {                                                          //  add balance to lower limit
            cropx1 = cropx1 - cropx2 + E0ww;
            cropx2 = E0ww;
         }
      }

      if (delta < 0) {                                                                 //  decreased width
         cropx1 = cropx1 - delta / 2;
         cropx2 = cropx2 + delta / 2;
         if (delta % 2) {
            cropx1 = cropx1 + flip;
            cropx2 = cropx2 - 1 + flip;
         }
      }

      if (cropx1 < 0) cropx1 = 0;                                                      //  keep within limits
      if (cropx2 > E0ww) cropx2 = E0ww;                                                //  use ww not ww-1
      cropww = cropx2 - cropx1;

      delta = height - crophh;                                                         //  change in height

      if (delta > 0) {                                                                 //  increased height
         cropy1 = cropy1 - delta / 2;                                                  //  top and bottom sides equally
         cropy2 = cropy2 + delta / 2;
         if (delta % 2) {                                                              //  if increase is odd
            cropy1 = cropy1 - flip;                                                    //    add 1 alternatively to each side
            cropy2 = cropy2 + 1 - flip;
         }
         if (cropy1 < 0) {
            cropy2 = cropy2 - cropy1;
            cropy1 = 0;
         }
         if (cropy2 > E0hh) {
            cropy1 = cropy1 - cropy2 + E0hh;
            cropy2 = E0hh;
         }
      }

      if (delta < 0) {                                                                 //  decreased height
         cropy1 = cropy1 - delta / 2;
         cropy2 = cropy2 + delta / 2;
         if (delta % 2) {
            cropy1 = cropy1 + flip;
            cropy2 = cropy2 - 1 + flip;
         }
      }

      if (cropy1 < 0) cropy1 = 0;                                                      //  keep within limits
      if (cropy2 > E0hh) cropy2 = E0hh;                                                //  use hh not hh-1
      crophh = cropy2 - cropy1;

      if (! Rlock) {
         cropR = 1.0 * width / height;                                                 //  new w/h ratio
         snprintf(text,20,"%.3f  ",cropR);
         zdialog_stuff(zd,"cropR",text);
      }

      crop_darkmargins();                                                              //  show crop area in image
      return 1;
   }

   if (! Rlock) cropR = 1.0 * width / height;
   snprintf(text,20,"%.3f  ",cropR);                                                   //  update crop ratio
   zdialog_stuff(zd,"cropR",text);

   for (ii = 0; ii < cropmem; ii++)                                                    //  crop ratio button pressed?
      if (strmatch(event,cropratios[ii])) break;
   if (ii < cropmem) {
      r1 = r2 = Fratio = 0;                                                            //  yes, get crop ratio
      pp = substring(cropratios[ii],':',1);
      if (pp) r1 = atofz(pp);
      pp = substring(cropratios[ii],':',2);
      if (pp) r2 = atofz(pp);
      if (r1 > 0 && r2 > 0) Fratio = r1/r2;
      if (Fratio < 0.1 || Fratio > 10) Fratio = 1.0;                                   //  reject extreme ratio
      else {
         zdialog_stuff(zd,"Rlock",1);                                                  //  assume ratio lock is wanted
         Rlock = 1;
      }
   }

   if (strmatch(event,"invert"))                                                       //  [invert] ratio button
      Fratio = 1.0 * crophh / cropww;

   if (Fratio)                                                                         //  w/h ratio was changed
   {
      cropR = Fratio;

      if (cropx2 - cropx1 > cropy2 - cropy1)                                           //  adjust smaller dimension
         cropy2 = cropy1 + (cropx2 - cropx1) / cropR + 0.5;                            //  round
      else
         cropx2 = cropx1 + (cropy2 - cropy1) * cropR + 0.5;

      if (cropx2 > E0ww) {                                                             //  if off the right edge,
         cropx2 = E0ww;                                                                //  adjust height
         cropy2 = cropy1 + (cropx2 - cropx1) / cropR + 0.5;
      }

      if (cropy2 > E0hh) {                                                             //  if off the bottom edge,
         cropy2 = E0hh;                                                                //  adjust width
         cropx2 = cropx1 + (cropy2 - cropy1) * cropR + 0.5;
      }

      cropww = cropx2 - cropx1;                                                        //  new rectangle dimensions
      crophh = cropy2 - cropy1;

      if (cropww > crophh) crophh = cropww / cropR + 0.5;
      else cropww = crophh * cropR + 0.5;

      zdialog_stuff(zd,"width",cropww);                                                //  stuff width, height, ratio
      zdialog_stuff(zd,"height",crophh);

      snprintf(text,20,"%.3f  ",cropR);
      zdialog_stuff(zd,"cropR",text);

      crop_darkmargins();                                                              //  update crop area in image
      return 1;
   }

   if (strmatch(event,"custom")) {                                                     //  [customize] button
      draw_toplines(2,0);                                                              //  erase crop rectangle
      erase_topcircles();                                                              //  erase center circle
      edit_cancel(0);                                                                  //  cancel edit
      crop_customize();                                                                //  customize dialog
      m_crop(0,0);                                                                     //  restart function
      return 1;
   }

   return 1;
}


//  crop size popup menu event function

void crop_names::prevcrop_menu_event(GtkWidget *, ch *menu)
{
   using namespace crop_names;

   int         nn, width, height;
   zdialog     *zd;

   nn = sscanf(menu,"%dx%d",&width,&height);                                           //  chosen crop size, "NNNNxNNNN"
   if (nn != 2) return;

   zd = EFcrop.zd;
   zdialog_stuff(zd,"width",width);                                                    //  update dialog
   zdialog_stuff(zd,"height",height);
   zdialog_send_event(zd,"width");

   zdialog_stuff(zd,"Rlock",0);                                                        //  set no ratio lock
   Rlock = 0;

   return;
}


//  update memory of previously used crop sizes, for future use

void crop_names::update_prev_cropsize()
{
   using namespace crop_names;

   int      ii, nn = cropmem;                                                          //  memory depth, fotocx.h 'cropmem'
   ch       mysize[20];

   snprintf(mysize,20,"%dx%d",cropww,crophh);                                          //  current crop size, "2345x1234"

   for (ii = 0; ii < nn; ii++)                                                         //  depth = last cropmem values
      if (strmatch(cropsizes[ii],mysize)) break;                                       //  must match fotocx.h 'cropmem'

   if (ii < nn) {                                                                      //  found in list
      zfree(cropsizes[ii]);                                                            //  remove from list
      for (ii++; ii < nn; ii++)                                                        //  move remaining entries up
         cropsizes[ii-1] = cropsizes[ii];
      cropsizes[nn-1] = 0;                                                             //  last = null
   }

   if (cropsizes[nn-1]) zfree(cropsizes[nn-1]);                                        //  discard oldest (last)
   for (ii = nn-1; ii > 0; ii--)                                                       //  move other entries up
      cropsizes[ii] = cropsizes[ii-1];
   cropsizes[0] = zstrdup(mysize,"crop");                                              //  entry 0 = this size
   return;
}


//  mouse function - move crop rectangle via mouse drags

void crop_names::mousefunc()
{
   using namespace crop_names;

   int      mpx, mpy, xdrag, ydrag;
   int      d1, d2, d3, d4;
   int      dc, ds, dx, dy, dz;
   int      cx, cy, close;
   float    fdc;

   mpx = mpy = xdrag = ydrag = 0;                                                      //  avoid gcc warnings

   if (! LMclick && ! RMclick && ! Mxdrag && ! Mydrag) {                               //  no click or drag underway
      Fcorner = Fside = Fcenter = 0;
      return;
   }

   if (Mxdrag || Mydrag)                                                               //  continue drag underway
   {
      mpx = Mxdrag;
      mpy = Mydrag;
      xdrag = Mxdrag - Mxdown;
      ydrag = Mydrag - Mydown;
      Mxdown = Mxdrag;                                                                 //  reset drag origin
      Mydown = Mydrag;
      Mxdrag = Mydrag = 0;

      xdrag = xdrag * Zratio;                                                          //  scale mouse drag
      ydrag = ydrag * Zratio;                                                          //    to full image size
   }

   mpx = mpx * Zratio + 0.5;                                                           //  scale mouse position
   mpy = mpy * Zratio + 0.5;                                                           //     to full image size

   if (Fcorner)                                                                        //  continue drag corner
   {
      if (Fcorner == 1) { cropx1 = mpx; cropy1 = mpy; }
      if (Fcorner == 2) { cropx2 = mpx; cropy1 = mpy; }
      if (Fcorner == 3) { cropx2 = mpx; cropy2 = mpy; }
      if (Fcorner == 4) { cropx1 = mpx; cropy2 = mpy; }
      goto crop_finish;
   }

   if (Fside)                                                                          //  continue drag side
   {
      if (Fside == 1) cropx1 = mpx;
      if (Fside == 2) cropy1 = mpy;
      if (Fside == 3) cropx2 = mpx;
      if (Fside == 4) cropy2 = mpy;
      goto crop_finish;
   }

   if (Fcenter)                                                                        //  continue drag entire crop rectangle
   {
      cropx1 += xdrag;
      cropx2 += xdrag;
      cropy1 += ydrag;
      cropy2 += ydrag;
      if (cropx1 < 0) cropx1 = 0;
      if (cropy1 < 0) cropy1 = 0;
      if (cropx2 > E0ww) cropx2 = E0ww;
      if (cropy2 > E0hh) cropy2 = E0hh;
      goto crop_finish;
   }

   if (E0ww > E0hh) close = 0.05 * E0ww;                                               //  mouse capture threshold
   else close = 0.05 * E0hh;                                                           //    = 5% of image size

   dx = mpx - cropx1;                                                                  //  find closest corner/side to mouse
   dy = mpy - cropy1;
   d1 = sqrt(dx*dx + dy*dy);                                                           //  distance from NW corner

   dx = mpx - cropx2;
   dy = mpy - cropy1;
   d2 = sqrt(dx*dx + dy*dy);                                                           //  NE

   dx = mpx - cropx2;
   dy = mpy - cropy2;
   d3 = sqrt(dx*dx + dy*dy);                                                           //  SE

   dx = mpx - cropx1;
   dy = mpy - cropy2;
   d4 = sqrt(dx*dx + dy*dy);                                                           //  SW

   Fcorner = 1;                                                                        //  find closest corner to mouse
   dc = d1;                                                                            //  NW
   if (d2 < dc) { Fcorner = 2; dc = d2; }                                              //  NE
   if (d3 < dc) { Fcorner = 3; dc = d3; }                                              //  SE
   if (d4 < dc) { Fcorner = 4; dc = d4; }                                              //  SW

   d1 = abs(mpx - cropx1);                                                             //  distance from left side
   d2 = abs(mpy - cropy1);                                                             //  top
   d3 = abs(mpx - cropx2);                                                             //  right
   d4 = abs(mpy - cropy2);                                                             //  bottom

   Fside = 1;                                                                          //  find closest side to mouse
   ds = d1;                                                                            //  left
   if (d2 < ds) { Fside = 2; ds = d2; }                                                //  top
   if (d3 < ds) { Fside = 3; ds = d3; }                                                //  right
   if (d4 < ds) { Fside = 4; ds = d4; }                                                //  bottom

   cx = (cropx1 + cropx2) * 0.5;                                                       //  center of crop rectangle
   cy = (cropy1 + cropy2) * 0.5;

   dx = mpx - cx;                                                                      //  get mouse distance from
   dy = mpy - cy;                                                                      //    center of crop rectangle
   dz = sqrtf(dx * dx + dy * dy);

   if (dz < dc && dz < ds) {                                                           //  if center closer, drag center
      Fcorner = Fside = KBcorner = KBside = 0;
      if (dz > close) return;                                                          //  not close enough
      Fcenter = 1;
      goto crop_finish;
   }

   Fcenter = 0;                                                                        //  side or corner is closer

   fdc = (cropx2 - cropx1 + cropy2 - cropy1);                                          //  crop rectangle size
   fdc = 1.0 * dc / fdc;                                                               //  corner distance / rectangle size
   if (fdc < 0.05) Fside = 0;                                                          //  if near corner, use corner
   else Fcorner = 0;                                                                   //  else use closest side

   if (Fcorner == 1) { cropx1 = mpx; cropy1 = mpy; }                                   //  move this corner to mouse
   if (Fcorner == 2) { cropx2 = mpx; cropy1 = mpy; }
   if (Fcorner == 3) { cropx2 = mpx; cropy2 = mpy; }
   if (Fcorner == 4) { cropx1 = mpx; cropy2 = mpy; }

   if (Fside == 1) cropx1 = mpx;                                                       //  move this side to mouse
   if (Fside == 2) cropy1 = mpy;
   if (Fside == 3) cropx2 = mpx;
   if (Fside == 4) cropy2 = mpy;

   KBcorner = Fcorner;                                                                 //  save last corner/side moved
   KBside = Fside;

crop_finish:

   crop_limits();                                                                      //  check margin limits and adjust if req.
   crop_darkmargins();                                                                 //  show crop area in image

   return;
}


//  Keyboard function
//  KB arrow keys tweak the last selected corner or side

void crop_names::KBfunc(int key)
{
   using namespace crop_names;

   int      xstep, ystep;

   xstep = ystep = 0;
   if (key == GDK_KEY_Left) xstep = -1;
   if (key == GDK_KEY_Right) xstep = +1;
   if (key == GDK_KEY_Up) ystep = -1;
   if (key == GDK_KEY_Down) ystep = +1;

   if (KBcorner == 1) {                                                                //  NW
      cropx1 += xstep;
      cropy1 += ystep;
   }

   if (KBcorner == 2) {                                                                //  NE
      cropx2 += xstep;
      cropy1 += ystep;
   }

   if (KBcorner == 3) {                                                                //  SE
      cropx2 += xstep;
      cropy2 += ystep;
   }

   if (KBcorner == 4) {                                                                //  SW
      cropx1 += xstep;
      cropy2 += ystep;
   }

   if (KBside == 1) cropx1 += xstep;                                                   //  left
   if (KBside == 2) cropy1 += ystep;                                                   //  top
   if (KBside == 3) cropx2 += xstep;                                                   //  right
   if (KBside == 4) cropy2 += ystep;                                                   //  bottom

   crop_limits();                                                                      //  check margin limits and adjust if req.
   crop_darkmargins();                                                                 //  show crop area in image
   return;
}


//  check new margins for sanity and enforce locked aspect ratio

void crop_names::crop_limits()
{
   using namespace crop_names;

   int         chop;
   int         ww, hh, ww2, hh2;
   float       drr;
   ch          text[20];
   zdialog     *zd = EFcrop.zd;

   if (cropx1 > cropx2-10) cropx1 = cropx2-10;                                         //  sanity limits
   if (cropy1 > cropy2-10) cropy1 = cropy2-10;

   if (cropx1 < 0) { cropx2 -= cropx1; cropx1 = 0; }
   if (cropy1 < 0) { cropy2 -= cropy1; cropy1 = 0; }
   if (cropx2 > E0ww) { cropx1 -= (cropx2 - E0ww); cropx2 = E0ww; }
   if (cropy2 > E0hh) { cropy1 -= (cropy2 - E0hh); cropy2 = E0hh; }

   if (Rlock && Fcorner) {                                                             //  w/h ratio locked
      if (Fcorner < 3)
         cropy1 = cropy2 - 1.0 * (cropx2 - cropx1) / cropR + 0.5;                      //  round
      else
         cropy2 = cropy1 + 1.0 * (cropx2 - cropx1) / cropR + 0.5;
   }

   else if (Rlock) {                                                                   //  default Fside                         25.1
      ww = cropx2 - cropx1;
      hh = cropy2 - cropy1;
      ww2 = ww;
      hh2 = hh;
      if (Fside == 1 || Fside == 3) hh2 = ww / cropR + 0.5;
      else ww2 = hh * cropR + 0.5;
      ww2 = (ww2 - ww) / 2;
      hh2 = (hh2 - hh) / 2;
      cropx1 -= ww2;
      cropx2 += ww2;
      cropy1 -= hh2;
      cropy2 += hh2;
   }

   chop = 0;
   if (cropx1 < 0) {                                                                   //  look for off the edge
      cropx1 = 0;                                                                      //    after corner move
      chop = 1;
   }

   if (cropx2 > E0ww) {
      cropx2 = E0ww;
      chop = 2;
   }

   if (cropy1 < 0) {
      cropy1 = 0;
      chop = 3;
   }

   if (cropy2 > E0hh) {
      cropy2 = E0hh;
      chop = 4;
   }

   if (Rlock && chop) {                                                                //  keep ratio if off edge
      if (chop < 3)
         cropy2 = cropy1 + 1.0 * (cropx2 - cropx1) / cropR + 0.5;                      //  round
      else
         cropx2 = cropx1 + 1.0 * (cropy2 - cropy1) * cropR + 0.5;
   }

   if (cropx1 > cropx2-10) cropx1 = cropx2-10;                                         //  sanity limits
   if (cropy1 > cropy2-10) cropy1 = cropy2-10;

   if (cropx1 < 0) cropx1 = 0;                                                         //  keep within visible area
   if (cropx2 > E0ww) cropx2 = E0ww;
   if (cropy1 < 0) cropy1 = 0;
   if (cropy2 > E0hh) cropy2 = E0hh;

   cropww = cropx2 - cropx1;                                                           //  new rectangle dimensions
   crophh = cropy2 - cropy1;

   drr = 1.0 * cropww / crophh;                                                        //  new w/h ratio
   if (! Rlock) cropR = drr;

   zdialog_stuff(zd,"width",cropww);                                                   //  stuff width, height, ratio
   zdialog_stuff(zd,"height",crophh);
   snprintf(text,20,"%.3f  ",cropR);
   zdialog_stuff(zd,"cropR",text);

   return;
}


//  Darken image pixels outside of current crop margins.
//  messy logic: update pixmaps only for changed pixels to increase speed

void crop_names::crop_darkmargins()
{
   using namespace crop_names;

   static int  Fbusy = 0;
   int         zx1, zx2, zy1, zy2;                                                     //  crop rectangle, reduced image scale
   int         ox1, oy1, ox2, oy2;                                                     //  outer crop rectangle
   int         nx1, ny1, nx2, ny2;                                                     //  inner crop rectangle
   int         px, py;
   float       *pix1, *pix3;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   if (Fbusy++) return;                                                                //  prevent re-entrance

   if (cropx1 < 0) cropx1 = 0;                                                         //  keep within visible area
   if (cropx2 > E0ww) cropx2 = E0ww;
   if (cropy1 < 0) cropy1 = 0;
   if (cropy2 > E0hh) cropy2 = E0hh;

   if (pcropx1 < cropx1) ox1 = pcropx1;                                                //  outer rectangle
   else ox1 = cropx1;
   if (pcropx2 > cropx2) ox2 = pcropx2;
   else ox2 = cropx2;
   if (pcropy1 < cropy1) oy1 = pcropy1;
   else oy1 = cropy1;
   if (pcropy2 > cropy2) oy2 = pcropy2;
   else oy2 = cropy2;

   if (pcropx1 > cropx1) nx1 = pcropx1;                                                //  inner rectangle
   else nx1 = cropx1;
   if (pcropx2 < cropx2) nx2 = pcropx2;
   else nx2 = cropx2;
   if (pcropy1 > cropy1) ny1 = pcropy1;
   else ny1 = cropy1;
   if (pcropy2 < cropy2) ny2 = pcropy2;
   else ny2 = cropy2;

   ox1 /= Zratio;                                                                      //  outer rectangle, reduced image scale
   oy1 /= Zratio;
   ox2 /= Zratio;
   oy2 /= Zratio;

   nx1 /= Zratio;                                                                      //  inner rectangle
   ny1 /= Zratio;
   nx2 /= Zratio;
   ny2 /= Zratio;

   ox1 -= 2;                                                                           //  expand outer rectangle
   oy1 -= 2;
   ox2 += 2;
   oy2 += 2;

   nx1 += 2;                                                                           //  reduce inner rectangle
   ny1 += 2;
   nx2 -= 2;
   ny2 -= 2;

   if (ox1 < 0) ox1 = 0;
   if (oy1 < 0) oy1 = 0;
   if (ox2 > Eww) ox2 = Eww;
   if (oy2 > Ehh) oy2 = Ehh;

   if (nx1 < ox1) nx1 = ox1;
   if (ny1 < oy1) ny1 = oy1;
   if (nx2 > ox2) nx2 = ox2;
   if (ny2 > oy2) ny2 = oy2;

   if (nx2 < nx1) nx2 = nx1;
   if (ny2 < ny1) ny2 = ny1;

   if (! E9pxm) E9pxm = PXM_copy(E3pxm);                                               //  E9 = original source image (reduced)

   zx1 = cropx1 / Zratio + 0.5;                                                        //  crop rectangle for reduced image
   zy1 = cropy1 / Zratio + 0.5;
   zx2 = cropx2 / Zratio + 0.5;
   zy2 = cropy2 / Zratio + 0.5;

   if (zx2 > Eww) zx2 = Eww;
   if (zy2 > Ehh) zy2 = Ehh;

   for (py = oy1; py < ny1; py++)                                                      //  top band of pixels
   for (px = ox1; px < ox2; px++)
   {
      pix1 = PXMpix(E9pxm,px,py);
      pix3 = PXMpix(E3pxm,px,py);

      if (px < zx1 || px >= zx2 || py < zy1 || py >= zy2)
      {
         pix3[0] = pix1[0] / 2;                                                        //  outside crop margins
         pix3[1] = pix1[1] / 2;                                                        //  50% brightness
         pix3[2] = pix1[2] / 2;
      }
      else memcpy(pix3,pix1,pcc);                                                      //  inside, full brightness
   }

   for (py = oy1; py < oy2; py++)                                                      //  right band
   for (px = nx2; px < ox2; px++)
   {
      pix1 = PXMpix(E9pxm,px,py);
      pix3 = PXMpix(E3pxm,px,py);

      if (px < zx1 || px >= zx2 || py < zy1 || py >= zy2)
      {
         pix3[0] = pix1[0] / 2;
         pix3[1] = pix1[1] / 2;
         pix3[2] = pix1[2] / 2;
      }
      else memcpy(pix3,pix1,pcc);
   }

   for (py = ny2; py < oy2; py++)                                                      //  bottom band
   for (px = ox1; px < ox2; px++)
   {
      pix1 = PXMpix(E9pxm,px,py);
      pix3 = PXMpix(E3pxm,px,py);

      if (px < zx1 || px >= zx2 || py < zy1 || py >= zy2)
      {
         pix3[0] = pix1[0] / 2;
         pix3[1] = pix1[1] / 2;
         pix3[2] = pix1[2] / 2;
      }
      else memcpy(pix3,pix1,pcc);
   }

   for (py = oy1; py < oy2; py++)                                                      //  left band
   for (px = ox1; px < nx1; px++)
   {
      pix1 = PXMpix(E9pxm,px,py);
      pix3 = PXMpix(E3pxm,px,py);

      if (px < zx1 || px >= zx2 || py < zy1 || py >= zy2)
      {
         pix3[0] = pix1[0] / 2;
         pix3[1] = pix1[1] / 2;
         pix3[2] = pix1[2] / 2;
      }
      else memcpy(pix3,pix1,pcc);
   }

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   Fpaint3(ox1,oy1,ox2-ox1,ny1-oy1,cr);                                                //  4 updated rectangles
   Fpaint3(nx2,oy1,ox2-nx2,oy2-oy1,cr);
   Fpaint3(ox1,ny2,ox2-ox1,oy2-ny2,cr);
   Fpaint3(ox1,oy1,nx1-ox1,oy2-oy1,cr);

   drawlines(cr);                                                                      //  draw crop margin lines

   draw_context_destroy(draw_context);

   if (ox1 == 0 || oy1 == 0 || ox2 == Eww || oy2 == Ehh)                               //  insure edges are refreshed
      Fpaint2();

   pcropx1 = cropx1;                                                                   //  prior crop rectangle
   pcropy1 = cropy1;                                                                   //    = current crop rectangle
   pcropx2 = cropx2;
   pcropy2 = cropy2;

   Fbusy = 0;
   return;
}


//  draw lines on image: crop rectangle, center circle

void crop_names::drawlines(cairo_t *cr)
{
   using namespace crop_names;

   int      cx, cy, rad, rad2;
   int      zx1, zx2, zy1, zy2;                                                        //  crop rectangle, reduced image scale

   zx1 = cropx1 / Zratio + 0.5;                                                        //  crop rectangle for reduced image
   zy1 = cropy1 / Zratio + 0.5;
   zx2 = cropx2 / Zratio + 0.5;
   zy2 = cropy2 / Zratio + 0.5;

   if (zx2 > Eww) zx2 = Eww;
   if (zy2 > Ehh) zy2 = Ehh;

   Ntoplines = 4;                                                                      //  outline crop rectangle
   toplines[0].x1 = zx1;
   toplines[0].y1 = zy1;
   toplines[0].x2 = zx2;
   toplines[0].y2 = zy1;
   toplines[0].type = 2;                                                               //  white lines
   toplines[1].x1 = zx2;
   toplines[1].y1 = zy1;
   toplines[1].x2 = zx2;
   toplines[1].y2 = zy2;
   toplines[1].type = 2;
   toplines[2].x1 = zx2;
   toplines[2].y1 = zy2;
   toplines[2].x2 = zx1;
   toplines[2].y2 = zy2;
   toplines[2].type = 2;
   toplines[3].x1 = zx1;
   toplines[3].y1 = zy2;
   toplines[3].x2 = zx1;
   toplines[3].y2 = zy1;
   toplines[3].type = 2;

   draw_toplines(1,cr);                                                                //  draw crop rectangle and guidelines

   erase_topcircles();                                                                 //  erase center circle

   rad = 15 / Mscale;                                                                  //  repaint center area
   rad2 = 2 * rad + 1;
   cx = (pcropx1 + pcropx2) * 0.5 / Zratio;
   cy = (pcropy1 + pcropy2) * 0.5 / Zratio;
   Fpaint3(cx-rad,cy-rad,rad2,rad2,cr);

   cx = (cropx1 + cropx2) * 0.5 / Zratio;                                              //  draw center circle
   cy = (cropy1 + cropy2) * 0.5 / Zratio;
   add_topcircle(cx,cy,10);
   draw_topcircles(cr);

   return;
}


//  final crop - cut margins off
//  E3 is the full size input image, maybe rotated
//  E3 is the output image from user crop rectangle

void crop_names::crop_final()
{
   using namespace crop_names;

   int      px1, py1, px2, py2;
   float    *pix3, *pix9;
   int      nc = E3pxm->nc, pcc = nc * sizeof(float);
   int      ww, hh, nn;

   ww = cropww;                                                                        //  save final width/height in user dialog
   hh = crophh;

   if (ww < 2 || hh < 2) return;

   nn = ww - (cropx2 - cropx1);                                                        //  adjust crop rectangle to hit
   cropx1 -= nn/2;                                                                     //    target width and height
   if (cropx1 < 0) cropx1 = 0;
   cropx2 = cropx1 + ww;
   if (cropx2 > Eww) cropx2 = Eww;

   nn = hh - (cropy2 - cropy1);
   cropy1 -= nn/2;
   if (cropy1 < 0) cropy1 = 0;
   cropy2 = cropy1 + hh;
   if (cropy2 > Ehh) cropy2 = Ehh;

   if (E9pxm) PXM_free(E9pxm);
   E9pxm = PXM_make(ww,hh,nc);                                                         //  new pixmap with requested size

   for (py1 = cropy1; py1 < cropy2; py1++)                                             //  copy E3 (rotated) to new size
   for (px1 = cropx1; px1 < cropx2; px1++)
   {
      px2 = px1 - cropx1;
      py2 = py1 - cropy1;
      pix3 = PXMpix(E3pxm,px1,py1);
      pix9 = PXMpix(E9pxm,px2,py2);
      memcpy(pix9,pix3,pcc);
   }

   PXM_free(E3pxm);
   E3pxm = E9pxm;
   E9pxm = 0;

   Eww = E3pxm->ww;                                                                    //  update E3 dimensions
   Ehh = E3pxm->hh;

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

   Fpaint2();
   return;
}


//  auto-crop image - set max. crop rectangle to exclude transparent regions

void crop_names::autocrop_max()
{
   using namespace crop_names;

   PXM      *pxm = 0;
   int      px1, py1, px2, py2;
   int      qx1, qy1, qx2, qy2;
   int      qx, qy, step1, step2;
   int      area1, area2, Fgrow = 0;
   float    *ppix;

   if (! E3pxm) return;                                                                //  no edit image
   if (! E0pxm) return;

   pxm = E0pxm;

   if (pxm->nc < 4) {                                                                  //  no alpha channel
      cropx1 = 0;
      cropx2 = pxm->ww;                                                                //  set max. dimensions
      cropy1 = 0;
      cropy2 = pxm->hh;
      cropww = pxm->ww;
      crophh = pxm->hh;
      return;
   }

   for (px1 = 0; px1 < pxm->ww; px1++) {                                               //  check image edges for transparency
      ppix = PXMpix(pxm,px1,0);
      if (ppix[3] < 200) break;
      ppix = PXMpix(pxm,px1,pxm->hh-1);
      if (ppix[3] < 200) break;
   }
   for (py1 = 0; py1 < pxm->hh; py1++) {
      ppix = PXMpix(pxm,0,py1);
      if (ppix[3] < 200) break;
      ppix = PXMpix(pxm,pxm->ww-1,py1);
      if (ppix[3] < 200) break;
   }

   if (px1 == pxm->ww && py1 == pxm->hh) {                                             //  no edge transparencies found
      cropx1 = 0;
      cropx2 = pxm->ww;                                                                //  set max. dimensions
      cropy1 = 0;
      cropy2 = pxm->hh;
      cropww = pxm->ww;
      crophh = pxm->hh;
      return;
   }

   px1 = 0.4 * pxm->ww;                                                                //  select small rectangle in the middle
   py1 = 0.4 * pxm->hh;
   px2 = 0.6 * pxm->ww;
   py2 = 0.6 * pxm->hh;

   step1 = 0.02 * (pxm->ww + pxm->hh);                                                 //  start with big search steps
   if (step1 < 2) step1 = 2;
   step2 = 0.2 * step1;
   if (step2 < 1) step2 = 1;

   while (true)
   {
      while (true)
      {
         Fgrow = 0;

         area1 = (px2 - px1) * (py2 - py1);                                            //  area of current selection rectangle
         area2 = area1;

         for (qx1 = px1-step1; qx1 <= px1+step1; qx1 += step2)                         //  loop, vary NW and SE corners +/-
         for (qy1 = py1-step1; qy1 <= py1+step1; qy1 += step2)
         for (qx2 = px2-step1; qx2 <= px2+step1; qx2 += step2)
         for (qy2 = py2-step1; qy2 <= py2+step1; qy2 += step2)
         {
            if (qx1 < 0) continue;                                                     //  check image limits
            if (qy1 < 0) continue;
            if (qx1 > 0.5 * pxm->ww) continue;
            if (qy1 > 0.5 * pxm->hh) continue;
            if (qx2 > pxm->ww-1) continue;
            if (qy2 > pxm->hh-1) continue;
            if (qx2 < 0.5 * pxm->ww) continue;
            if (qy2 < 0.5 * pxm->hh) continue;

            ppix = PXMpix(pxm,qx1,qy1);                                                //  check 4 corners are not
            if (ppix[3] < 200) continue;                                               //    in transparent zones
            ppix = PXMpix(pxm,qx2,qy1);
            if (ppix[3] < 200) continue;
            ppix = PXMpix(pxm,qx2,qy2);
            if (ppix[3] < 200) continue;
            ppix = PXMpix(pxm,qx1,qy2);
            if (ppix[3] < 200) continue;

            area2 = (qx2 - qx1) * (qy2 - qy1);                                         //  look for larger enclosed area
            if (area2 <= area1) continue;

            for (qx = qx1; qx < qx2; qx++) {                                           //  check top/bottom sides not intersect
               ppix = PXMpix(pxm,qx,qy1);                                              //    transparent zones
               if (ppix[3] < 200) break;
               ppix = PXMpix(pxm,qx,qy2);
               if (ppix[3] < 200) break;
            }
            if (qx < qx2) continue;

            for (qy = qy1; qy < qy2; qy++) {                                           //  also left/right sides
               ppix = PXMpix(pxm,qx1,qy);
               if (ppix[3] < 200) break;
               ppix = PXMpix(pxm,qx2,qy);
               if (ppix[3] < 200) break;
            }
            if (qy < qy2) continue;

            area1 = area2;
            Fgrow = 1;                                                                 //  successfully grew the rectangle
            px1 = qx1;                                                                 //  new bigger rectangle coordinates
            py1 = qy1;                                                                 //    for the next search iteration
            px2 = qx2;
            py2 = qy2;
         }

         if (! Fgrow) break;
      }

      if (step2 == 1) break;                                                           //  done

      step1 = 0.7 * step1;                                                             //  reduce search step size
      if (step1 < 2) step1 = 2;
      step2 = 0.2 * step1;
      if (step2 < 1) step2 = 1;
   }

   cropx1 = px1;                                                                       //  set parameters for crop function
   cropx2 = px2;
   cropy1 = py1;
   cropy2 = py2;

   cropww = px2 - px1;
   crophh = py2 - py1;

   return;
}


//  dialog to set custom crop button names and corresponding ratios

void crop_names::crop_customize()
{
   using namespace crop_names;

   ch          text[20], rlab[8];
   float       r1, r2, ratio = 0;
   ch          *pp;
   int         ii, zstat;

/***
       ____________________________________________
      |               Crop Buttons                 |
      |                                            |
      |  [ 5:4 ] [ 4:3 ] [ 8:5 ] [ 16:9 ] [ 2:1 ]  |
      |                                            |
      |                                   [OK] [X] |
      |____________________________________________|

***/

   zdialog *zd = zdialog_new(TX("Crop Buttons"),Mwin,"OK","X",null);
   zdialog_add_widget(zd,"hbox","hbr","dialog",0,"homog|space=3");

   strcpy(rlab,"ratio-0");

   for (ii = 0; ii < cropmem; ii++)                                                    //  add crop ratio buttons to dialog
   {
      rlab[6] = '0' + ii;
      zdialog_add_widget(zd,"zentry",rlab,"hbr",cropratios[ii],"size=4");
   }

   zdialog_set_modal(zd);
   zdialog_run(zd,0,"mouse");                                                          //  run dialog
   zstat = zdialog_wait(zd);                                                           //  wait for complete

   if (zstat == 1)                                                                     //  done
   {
      for (ii = 0; ii < cropmem; ii++)                                                 //  get new ratios
      {
         rlab[6] = '0' + ii;
         zdialog_fetch(zd,rlab,text,12);
         strTrim2(text);
         r1 = r2 = ratio = 0;
         pp = substring(text,':',1);
         if (! pp) continue;
         r1 = atofz(pp);
         pp = substring(text,':',2);
         if (! pp) continue;
         r2 = atofz(pp);
         if (r1 > 0 && r2 > 0) ratio = r1/r2;
         if (ratio < 0.1 || ratio > 10) continue;
         zstrcopy(cropratios[ii],text,"crop buttons");
      }
   }

   zdialog_free(zd);                                                                   //  kill dialog
   save_params();                                                                      //  save parameters

   return;
}


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

//  Retouch
//  Adjust all aspects of brightness, contrast, color:
//  Black level, white balance, brightness, contrast, color temperature, color saturation

namespace retouch_names
{
   editfunc    EFretouch;
   float       blackR, blackG, blackB;                                                 //  black level RGB values
   float       Rbscale, Gbscale, Bbscale;                                              //  scaled RGB factors - black clip
   float       wbalR, wbalG, wbalB;                                                    //  white balance RGB factors
   int         Fblack;                                                                 //  flag, black level was set
   int         sampWB, sampBL;                                                         //  sample size, % pixels
   int         spotWB, spotBL;                                                         //  spot white balance or black level
   float       tempR, tempG, tempB;                                                    //  illumination temperature RGB factors
   float       combR, combG, combB;                                                    //  combined RGB factors
   float       brightness;                                                             //  brightness input, -1 ... +1
   float       contrast;                                                               //  contrast input, -1 ... +1
   float       colorsat;                                                               //  saturation input, -1 ... +1
   int         colortemp;                                                              //  illumination temp. input, 1K ... 8K
   int         Fapply;                                                                 //  flag, apply dialog controls to image
   float       whitebalance[4] = { 1.0, 1.0, 1.0, 5000 };                              //  white balance RGB factors, deg. K
}


//  menu function

void m_retouch(GtkWidget *, ch *menu)
{
   using namespace retouch_names;

   void   retouch_mousefunc();
   int    retouch_dialog_event(zdialog* zd, ch *event);
   void   retouch_curvedit(int spc);
   void * retouch_thread(void *);

   GtkWidget   *drawwin_scale;

   F1_help_topic = TX("RETOUCH");

   printf("m_retouch \n");

   EFretouch.menuname = "Retouch";
   EFretouch.menufunc = m_retouch;
   EFretouch.FprevReq = 1;                                                             //  use preview
   EFretouch.Farea = 2;                                                                //  select area usable
   EFretouch.Fpaintedits = 1;                                                          //  use with paint edits OK
   EFretouch.Fscript = 1;                                                              //  scripting supported
   EFretouch.threadfunc = retouch_thread;
   EFretouch.mousefunc = retouch_mousefunc;

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

/***
       ____________________________________________
      |                 Retouch                    |
      |  ________________________________________  |
      | |                                        | |                                   //  5 curves are maintained:
      | |                                        | |                                   //  0: current display curve
      | |          curve edit area               | |                                   //  1: curve for overall brightness
      | |                                        | |                                   //  2, 3, 4: red, green, blue
      | |________________________________________| |
      | |________________________________________| |                                   //  brightness scale: black to white stripe
      |   (o) all  (o) red  (o) green  (o) blue    |                                   //  select curve to display
      |                                            |
      |  [_] Auto black level     [__] sample %    |                                   //  auto black level
      |  [_] Auto white balance   [__] sample %    |                                   //  auto white balance
      |  [x] Click gray spot for white balance     |                                   //  click gray spot
      |  [x] Click dark spot for black level       |                                   //  click dark spot
      |  [_] Click for RGB histogram               |                                   //  show RGB histogram in popup window
      |                                            |
      |  Brightness =============[]=============== |                                   //  brightness
      |   Contrast  ===============[]============= |                                   //  contrast
      |  Saturation  =================[]========== |                                   //  saturation B/W ... saturated
      |  Temperature ===============[]============ |                                   //  illumination temperature
      |                                            |
      |                    [Reset] [Prev] [OK] [X] |
      |____________________________________________|

***/

   zdialog *zd = zdialog_new(TX("Retouch"),Mwin,"Reset","Prev","OK","X",null);
   EFretouch.zd = zd;

   zdialog_add_widget(zd,"frame","frameH","dialog",0,"expand");                        //  edit-curve
   zdialog_add_widget(zd,"frame","frameB","dialog");                                   //  black to white brightness scale

   zdialog_add_widget(zd,"hbox","hbrgb","dialog");                                     //  (o) all  (o) red  (o) green  (o) blue
   zdialog_add_widget(zd,"radio","all","hbrgb",TX("All"),"space=5");
   zdialog_add_widget(zd,"radio","red","hbrgb",TX("Red"),"space=3");
   zdialog_add_widget(zd,"radio","green","hbrgb",TX("Green"),"space=3");
   zdialog_add_widget(zd,"radio","blue","hbrgb",TX("Blue"),"space=3");

   zdialog_add_widget(zd,"hbox","hbabl","dialog");                                     //  black level
   zdialog_add_widget(zd,"zbutton","autoBL","hbabl",TX("Auto black level"),"space=3");
   zdialog_add_widget(zd,"label","space","hbabl",0,"space=5");
   zdialog_add_widget(zd,"zspin","sampBL","hbabl","1|50|1|1","space=3|size=3");
   zdialog_add_widget(zd,"label","labsamp","hbabl",TX("sample %"));

   zdialog_add_widget(zd,"hbox","hbawb","dialog");                                     //  white balance
   zdialog_add_widget(zd,"zbutton","autoWB","hbawb",TX("Auto white balance"),"space=3");
   zdialog_add_widget(zd,"label","space","hbawb",0,"space=5");
   zdialog_add_widget(zd,"zspin","sampWB","hbawb","1|50|1|1","space=3|size=3");
   zdialog_add_widget(zd,"label","labsamp","hbawb",TX("sample %"));

   zdialog_add_widget(zd,"hbox","hbwb","dialog");                                      //  click gray spot
   zdialog_add_widget(zd,"check","spotWB","hbwb",TX("Click gray spot for white balance"),"space=3");

   zdialog_add_widget(zd,"hbox","hbbl","dialog");                                      //  click dark spot
   zdialog_add_widget(zd,"check","spotBL","hbbl",TX("Click dark spot for black level"),"space=3");

   zdialog_add_widget(zd,"hbox","hbhist","dialog");                                    //  click RGB histogram
   zdialog_add_widget(zd,"zbutton","hist","hbhist",TX("Click for RGB histogram"),"space=3");

   zdialog_add_widget(zd,"hbox","hbcolor","dialog",0,"space=8");
   zdialog_add_widget(zd,"label","space","hbcolor",0,"space=1");
   zdialog_add_widget(zd,"vbox","vbcolor1","hbcolor",0,"homog");
   zdialog_add_widget(zd,"label","space","hbcolor",0,"space=1");
   zdialog_add_widget(zd,"vbox","vbcolor2","hbcolor",0,"homog|expand");

   zdialog_add_widget(zd,"label","labrite","vbcolor1",TX("Brightness"));
   zdialog_add_widget(zd,"label","labcont","vbcolor1",TX("Contrast"));
   zdialog_add_widget(zd,"label","labsat1","vbcolor1",TX("Saturation"));
   zdialog_add_widget(zd,"label","labtemp1","vbcolor1",TX("Temperature"));

   zdialog_add_widget(zd,"hscale2","brightness","vbcolor2","-1.0|1.0|0.01|0");
   zdialog_add_widget(zd,"hscale2","contrast","vbcolor2","-1.0|1.0|0.01|0");
   zdialog_add_widget(zd,"hscale2","colorsat","vbcolor2","-1.0|1.0|0.01|0");
   zdialog_add_widget(zd,"hscale2","colortemp","vbcolor2","2000|8000|1|5000");

   zdialog_add_ttip(zd,"Prev",TX("use previous input"));

   GtkWidget *frameH = zdialog_gtkwidget(zd,"frameH");                                 //  setup edit curves
   spldat *sd = splcurve_init(frameH,retouch_curvedit);
   EFretouch.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;

   for (int ii = 0; ii < 4; ii++)                                                      //  loop curves 0-3
   {
      sd->nap[ii] = 3;                                                                 //  initial curves are neutral
      sd->vert[ii] = 0;
      sd->fact[ii] = 0;
      sd->apx[ii][0] = 0.01;                                                           //  horizontal lines
      sd->apy[ii][0] = 0.50;
      sd->apx[ii][1] = 0.50;
      sd->apy[ii][1] = 0.50;                                                           //  curve 0 = overall brightness
      sd->apx[ii][2] = 0.99;
      sd->apy[ii][2] = 0.50;                                                           //  curve 1/2/3 = R/G/B adjustment
      splcurve_generate(sd,ii);
      sd->mod[ii] = 0;                                                                 //  mark curve unmodified
   }

   sd->Nspc = 4;                                                                       //  4 curves
   sd->fact[0] = 1;                                                                    //  curve 0 active
   zdialog_stuff(zd,"all",1);                                                          //  stuff default selection, all

   GtkWidget *frameB = zdialog_gtkwidget(zd,"frameB");                                 //  setup brightness scale drawing area
   drawwin_scale = gtk_drawing_area_new();
   gtk_container_add(GTK_CONTAINER(frameB),drawwin_scale);
   gtk_widget_set_size_request(drawwin_scale,100,12);
   G_SIGNAL(drawwin_scale,"draw",brightness_scale,0);

   wbalR = wbalG = wbalB = 1.0;                                                        //  neutral white balance
   colortemp = 5000;                                                                   //  neutral illumination temp.
   blackbodyRGB(colortemp,tempR,tempG,tempB);                                          //  neutral temp. RGB factors
   combR = combG = combB = 1.0;                                                        //  neutral combined RGB factors
   blackR = blackG = blackB = 0.0;                                                     //  zero black point
   Fblack = 0;

   zdialog_stuff(zd,"spotWB",0);                                                       //  reset mouse click status
   zdialog_stuff(zd,"spotBL",0);
   spotWB = spotBL = 0;

   brightness = 0;                                                                     //  neutral brightness
   contrast = 0;                                                                       //  neutral contrast
   colorsat = 0;                                                                       //  neutral saturation
   Fapply = 0;

   zdialog_resize(zd,350,450);
   zdialog_run(zd,retouch_dialog_event,"save");                                        //  run dialog - parallel

   return;
}


//  dialog event and completion callback function

int retouch_dialog_event(zdialog *zd, ch *event)
{
   using namespace retouch_names;

   void  retouch_mousefunc();
   void  retouch_autoWB();
   void  retouch_autoBL();

   spldat      *sd = EFretouch.sd;
   float       bright0, dbrite, cont0, dcont;
   float       dx, dy;
   int         ii;

   Fapply = 0;

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

   if (strmatch(event,"fullsize")) {                                                   //  from select area
      edit_fullsize();                                                                 //  get full size image
      thread_signal();
      thread_wait();                                                                   //  required for paint edits
      return 1;
   }

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

      zdialog_stuff(zd,"brightness",0);                                                //  neutral brightness
      zdialog_stuff(zd,"contrast",0);                                                  //  neutral contrast
      brightness = contrast = 0;
      zdialog_stuff(zd,"colorsat",0);                                                  //  neutral saturation
      colorsat = 0;

      for (int ii = 0; ii < 4; ii++)                                                   //  loop brightness curves 0-3
      {
         sd->nap[ii] = 3;                                                              //  all curves are neutral
         sd->vert[ii] = 0;
         sd->fact[ii] = 0;
         sd->apx[ii][0] = 0.01;
         sd->apy[ii][0] = 0.50;
         sd->apx[ii][1] = 0.50;
         sd->apy[ii][1] = 0.50;
         sd->apx[ii][2] = 0.99;
         sd->apy[ii][2] = 0.50;
         splcurve_generate(sd,ii);
         sd->mod[ii] = 0;                                                              //  mark curve unmodified
      }

      sd->fact[0] = 1;                                                                 //  active curve is 'all'
      gtk_widget_queue_draw(sd->drawarea);                                             //  redraw curves

      zdialog_stuff(zd,"all",1);
      zdialog_stuff(zd,"red",0);
      zdialog_stuff(zd,"green",0);
      zdialog_stuff(zd,"blue",0);

      zdialog_stuff(zd,"spotWB",0);
      zdialog_stuff(zd,"spotBL",0);
      wbalR = wbalG = wbalB = 1.0;                                                     //  neutral white balance
      colortemp = 5000;                                                                //  neutral illumination temp.
      zdialog_stuff(zd,"colortemp",colortemp);
      blackbodyRGB(colortemp,tempR,tempG,tempB);                                       //  neutral temp. RGB factors
      combR = combG = combB = 1.0;                                                     //  neutral combined RGB factors
      blackR = blackG = blackB = 0.0;                                                  //  zero black point
      Fblack = 0;

      edit_reset();
      return 1;
   }

   if (zd->zstat == 2)                                                                 //  [prev] restore previous settings
   {
      zd->zstat = 0;                                                                   //  keep dialog active
      zdialog_load_prev_widgets(zd,sd,"retouch");
      wbalR = whitebalance[0];                                                         //  use previous white balance settings
      wbalG = whitebalance[1];
      wbalB = whitebalance[2];
      colortemp = whitebalance[3];
      zdialog_stuff(zd,"colortemp",colortemp);
      Fapply = 1;                                                                      //  trigger apply event
   }

   if (zd->zstat == 3)                                                                 //  [OK]
   {
      freeMouse();
      if (CEF->Fmods) {
         edit_fullsize();                                                              //  get full size image
         thread_signal();                                                              //  apply changes
         zdialog_save_last_widgets(zd,sd,"retouch");                                   //  save settings
         whitebalance[0] = wbalR;                                                      //  save settings for next time
         whitebalance[1] = wbalG;
         whitebalance[2] = wbalB;
         whitebalance[3] = colortemp;

         edit_addhist("br:%.2f con:%.2f sat:%.2f temp:%d",                             //  edit params > edit hist
                        brightness,contrast,colorsat,colortemp);
         edit_done(0);                                                                 //  complete edit
      }

      else edit_cancel(0);                                                             //  no changes made
      return 1;
   }

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

   if (strmatch(event,"autoWB")) {                                                     //  auto white balance
      zdialog_fetch(zd,"sampWB",sampWB);                                               //  % brightest pixels to sample
      zdialog_stuff(zd,"spotWB",0);
      zdialog_stuff(zd,"spotBL",0);
      spotWB = spotBL = 0;
      retouch_autoWB();
   }

   if (strmatch(event,"autoBL")) {                                                     //  auto black level
      zdialog_fetch(zd,"sampBL",sampBL);                                               //  % darkest pixels to sample
      zdialog_stuff(zd,"spotWB",0);
      zdialog_stuff(zd,"spotBL",0);
      spotWB = spotBL = 0;
      retouch_autoBL();
   }

   if (strmatch(event,"spotWB")) {                                                     //  spot white balance checkbox
      zdialog_fetch(zd,"spotWB",spotWB);
      if (spotWB) {
         zdialog_stuff(zd,"spotBL",0);
         spotBL = 0;
      }
   }

   if (strmatch(event,"spotBL")) {                                                     //  spot black level checkbox
      zdialog_fetch(zd,"spotBL",spotBL);
      if (spotBL) {
         zdialog_stuff(zd,"spotWB",0);
         spotWB = 0;
      }
   }

   if (zstrstr("spotWB spotBL",event)) {                                               //  spot white balance or black level
      if (spotWB || spotBL)                                                            //  if either is on,
         takeMouse(retouch_mousefunc,dragcursor);                                      //    connect mouse function
      else freeMouse();                                                                //  both off: free mouse
      return 1;
   }

   if (strmatch(event,"hist")) m_RGB_hist(0,0);                                        //  popup RGB histogram window

   if (zstrstr("all red green blue",event))                                            //  new choice of curve
   {
      zdialog_fetch(zd,event,ii);
      if (! ii) return 0;                                                              //  button OFF event, wait for ON event

      for (ii = 0; ii < 4; ii++) sd->fact[ii] = 0;
      ii = strmatchV(event,"all","red","green","blue",null);
      ii = ii-1;                                                                       //  new active curve: 0, 1, 2, 3
      sd->fact[ii] = 1;

      splcurve_generate(sd,ii);                                                        //  regenerate curve
      gtk_widget_queue_draw(sd->drawarea);                                             //  draw curve
   }

   if (strmatch(event,"brightness"))                                                   //  brightness slider, 0 ... 1
   {
      bright0 = brightness;
      zdialog_fetch(zd,"brightness",brightness);
      dbrite = brightness - bright0;                                                   //  change in brightness, + -

      zdialog_stuff(zd,"all",1);                                                       //  active curve is "all"
      sd->fact[0] = 1;
      for (ii = 1; ii < 4; ii++)
         sd->fact[ii] = 0;

      for (ii = 0; ii < sd->nap[0]; ii++)                                              //  update curve 0 nodes from slider
      {
         dy = sd->apy[0][ii] + 0.5 * dbrite;                                           //  increment brightness
         if (dy < 0) dy = 0;
         if (dy > 1) dy = 1;
         sd->apy[0][ii] = dy;
      }

      splcurve_generate(sd,0);                                                         //  regenerate curve 0
      gtk_widget_queue_draw(sd->drawarea);                                             //  draw curve 0
   }

   if (strmatch(event,"contrast"))                                                     //  contrast slider, 0 ... 1
   {
      cont0 = contrast;
      zdialog_fetch(zd,"contrast",contrast);
      dcont = contrast - cont0;                                                        //  change in contrast, + -

      zdialog_stuff(zd,"all",1);                                                       //  active curve is "all"
      sd->fact[0] = 1;
      for (ii = 1; ii < 4; ii++)
         sd->fact[ii] = 0;

      for (ii = 0; ii < sd->nap[0]; ii++)                                              //  update curve 0 nodes from slider
      {
         dx = sd->apx[0][ii];                                                          //  0 ... 0.5 ... 1
         if (dx < 0.0 || dx >= 1.0) continue;
         dy = dcont * (dx - 0.5);                                                      //  -0.5 ... 0 ... +0.5 * dcont
         dy += sd->apy[0][ii];
         if (dy < 0) dy = 0;
         if (dy > 1) dy = 1;
         sd->apy[0][ii] = dy;
      }

      splcurve_generate(sd,0);                                                         //  regenerate curve 0
      gtk_widget_queue_draw(sd->drawarea);                                             //  draw curve 0
   }

   if (strmatch(event,"paint")) Fapply = 1;                                            //  from paint_edits()

   if (zstrstr("brightness contrast colortemp colorsat "                               //  detect change
               "autoWB autoBL load",event)) Fapply = 1;

   if (! Fapply) return 1;                                                             //  wait for change

   zdialog_fetch(zd,"colortemp",colortemp);                                            //  get illumination temp. setting
   blackbodyRGB(colortemp,tempR,tempG,tempB);                                          //  generate temp. adjustments

   zdialog_fetch(zd,"brightness",brightness);                                          //  get brightness setting
   zdialog_fetch(zd,"contrast",contrast);                                              //  get contrast setting
   zdialog_fetch(zd,"colorsat",colorsat);                                              //  get saturation setting

   thread_signal();                                                                    //  update the image
   return 1;
}


//  this function is called when a curve is edited

void retouch_curvedit(int spc)
{
   using namespace retouch_names;
   thread_signal();
   return;
}


//  use brightest pixels to estimate RGB values of illumination
//  and use these to adjust image RGB values for neutral illumination

void retouch_autoWB()
{
   using namespace retouch_names;

   int         pixR[256], pixG[256], pixB[256];                                        //  pixel counts per RGB value 0-255
   int         samplesize;                                                             //  sample size, pixel count
   int         px, py, ii, dist;
   int         sampR, sampG, sampB;
   double      sumR, sumG, sumB;
   float       meanR, meanG, meanB;
   float       *pix;

   for (ii = 0; ii < 256; ii++)
      pixR[ii] = pixG[ii] = pixB[ii] = 0;

   for (py = 0; py < Ehh; py++)                                                        //  scan image file, get pixel counts
   for (px = 0; px < Eww; px++)                                                        //    per RGB value 0-255
   {
      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
      }

      pix = PXMpix(E1pxm,px,py);
      ii = pix[0];                                                                     //  pixel R value 0-255
      pixR[ii]++;                                                                      //  accumulate pixel count for R value
      ii = pix[1];
      pixG[ii]++;
      ii = pix[2];
      pixB[ii]++;
   }

   samplesize = Eww * Ehh;                                                             //  image pixel count
   if (sa_stat == sa_stat_fini) samplesize = sa_Npixel;                                //  use area count
   samplesize = samplesize * 0.01 * sampWB;                                            //  % sample size >> sample pixel count
   sampR = sampG = sampB = 0;
   sumR = sumG = sumB = 0;

   for (ii = 254; ii > 0; ii--)                                                        //  sample brightest % pixels
   {                                                                                   //  (avoid blowout value 255)
      if (sampR < samplesize) {
         sampR += pixR[ii];                                                            //  pixels with R value ii
         sumR += ii * pixR[ii];                                                        //  sum product
      }

      if (sampG < samplesize) {
         sampG += pixG[ii];
         sumG += ii * pixG[ii];
      }

      if (sampB < samplesize) {
         sampB += pixB[ii];
         sumB += ii * pixB[ii];
      }
   }

   meanR = sumR / sampR;                                                               //  mean RGB for brightest % pixels
   meanG = sumG / sampG;
   meanB = sumB / sampB;

   wbalR = meanG / meanR;                                                              //  = 1.0 if all RGB are equal
   wbalG = meanG / meanG;                                                              //  <1/>1 if RGB should be less/more
   wbalB = meanG / meanB;

   return;
}


//  use darkest pixels to estimate black point and reduce image RGB values

void retouch_autoBL()
{
   using namespace retouch_names;

   int         pixR[256], pixG[256], pixB[256];                                        //  pixel counts per RGB value 0-255
   int         samplesize;                                                             //  sample size, pixel count
   int         px, py, ii, dist;
   int         sampR, sampG, sampB;
   double      sumR, sumG, sumB;
   float       meanR, meanG, meanB;
   float       *pix;

   for (ii = 0; ii < 256; ii++)
      pixR[ii] = pixG[ii] = pixB[ii] = 0;

   for (py = 0; py < Ehh; py++)                                                        //  scan image file, get pixel counts
   for (px = 0; px < Eww; px++)                                                        //    per RGB value 0-255
   {
      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
      }

      pix = PXMpix(E1pxm,px,py);
      ii = pix[0];                                                                     //  pixel R value 0-255
      pixR[ii]++;                                                                      //  accumulate pixel count for R value
      ii = pix[1];
      pixG[ii]++;
      ii = pix[2];
      pixB[ii]++;
   }

   samplesize = Eww * Ehh;                                                             //  image pixel count
   if (sa_stat == sa_stat_fini) samplesize = sa_Npixel;                                //  use area count
   samplesize = samplesize * 0.01 * sampBL;                                            //  % sample size >> sample pixel count
   sampR = sampG = sampB = 0;
   sumR = sumG = sumB = 0;

   for (ii = 0; ii < 100; ii++)                                                        //  sample darkest % pixels
   {
      if (sampR < samplesize) {
         sampR += pixR[ii];                                                            //  pixels with R value ii
         sumR += ii * pixR[ii];                                                        //  sum product
      }

      if (sampG < samplesize) {
         sampG += pixG[ii];
         sumG += ii * pixG[ii];
      }

      if (sampB < samplesize) {
         sampB += pixB[ii];
         sumB += ii * pixB[ii];
      }
   }

   meanR = sumR / sampR;                                                               //  mean RGB for darkest % pixels
   meanG = sumG / sampG;
   meanB = sumB / sampB;

   blackR = meanR;                                                                     //  set black RGB levels
   blackG = meanG;
   blackB = meanB;
   Fblack = 1;

   return;
}


//  get nominal white color or black point from mouse click position

void retouch_mousefunc()                                                               //  mouse function
{
   using namespace retouch_names;

   int         px, py, dx, dy;
   float       red, green, blue;
   float       *ppix;
   ch          mousetext[60];

   if (! LMclick) return;
   LMclick = 0;

   px = Mxclick;                                                                       //  mouse click position
   py = Myclick;

   if (px < 2) px = 2;                                                                 //  pull back from edge
   if (px > Eww-3) px = Eww-3;
   if (py < 2) py = 2;
   if (py > Ehh-3) py = Ehh-3;

   red = green = blue = 0;

   for (dy = -1; dy <= 1; dy++)                                                        //  3x3 block around mouse position
   for (dx = -1; dx <= 1; dx++)
   {
      ppix = PXMpix(E1pxm,px+dx,py+dy);                                                //  input image
      red += ppix[0];
      green += ppix[1];
      blue += ppix[2];
   }

   red = red / 9.0;                                                                    //  mean RGB levels
   green = green / 9.0;
   blue = blue / 9.0;

   snprintf(mousetext,60,"3x3 Pixels RGB: %.0f %.0f %.0f \n",red,green,blue);
   poptext_mouse(TX(mousetext),10,10,0,3);

   if (spotWB &&
      (red<5 || red>250 || green<5 || green>250 || blue<5 || blue>250)) {              //  refuse unscalable spot
      zmessageACK(Mwin,TX("choose a better spot"));
      return;
   }

   if (spotWB) {                                                                       //  click pixel is new gray/white
      wbalR = green / red;                                                             //  = 1.0 if all RGB are equal
      wbalG = green / green;                                                           //  <1/>1 if RGB should be less/more
      wbalB = green / blue;
      thread_signal();                                                                 //  trigger image update
   }

   if (spotBL) {                                                                       //  click pixel is new black point
      blackR = red;
      blackG = green;
      blackB = blue;
      Fblack = 1;
      thread_signal();                                                                 //  update image
   }

   return;
}


//  thread function

void * retouch_thread(void *arg)
{
   using namespace retouch_names;

   void * retouch_wthread(void *);

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

   combR = wbalR * tempR / 256.0;                                                      //  combine white balance colors
   combG = wbalG * tempG / 256.0;                                                      //    and illumination temperature.
   combB = wbalB * tempB / 256.0;                                                      //      <1/>1 if RGB should be less/more

   Rbscale = Gbscale = Bbscale = 1.0;
   if (Fblack) {                                                                       //  if RGB black points defined,
      Rbscale = 256.0 / (256.0 - blackR);                                              //    rescale for full brightness
      Gbscale = 256.0 / (256.0 - blackG);
      Bbscale = 256.0 / (256.0 - blackB);
   }

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

   do_wthreads(retouch_wthread,NSMP);                                                  //  do working threads

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

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


void * retouch_wthread(void *arg)                                                      //  worker thread function
{
   using namespace retouch_names;

   int         index = *((int *) arg);
   int         ii, px, py, Fend;
   float       *pix1, *pix3;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       pixbrite, ff, F1, F2;
   float       blend, coeff = 1000.0 / 256.0;
   spldat      *sd = EFretouch.sd;

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

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

      //  apply RGB black points to input RGB values

      if (Fblack) {
         R9 = R9 - blackR;                                                             //  clip black value at low end
         R9 = R9 * Rbscale;                                                            //  rescale so that high end = 256
         if (R9 < 0) R9 = 0;
         G9 = G9 - blackG;
         G9 = G9 * Gbscale;
         if (G9 < 0) G9 = 0;
         B9 = B9 - blackB;
         B9 = B9 * Bbscale;
         if (B9 < 0) B9 = 0;
      }

      //  apply white balance and temperature color shifts                             //  dampen as RGB values approach 256

      if (combR > 1) {
         F1 = (256 - R9) / 256;                                                        //  R1 = 0...256  >>  F1 = 1...0
         F2 = F1 * combR + 1 - F1;                                                     //  F2 = combR ... 1
         R9 = F2 * R9;                                                                 //  R9 is adjusted R1
      }
      else R9 = combR * R9;

      if (combG > 1) {                                                                 //  same for G9 and B9
         F1 = (256 - G9) / 256;
         F2 = F1 * combG + 1 - F1;
         G9 = F2 * G9;
      }
      else G9 = combG * G9;

      if (combB > 1) {
         F1 = (256 - B9) / 256;
         F2 = F1 * combB + 1 - F1;
         B9 = F2 * B9;
      }
      else B9 = combB * B9;

      //  apply saturation color shift

      if (colorsat != 0) {
         pixbrite = 0.333 * (R9 + G9 + B9);                                            //  pixel brightness, 0 to 255.9
         R9 = R9 + colorsat * (R9 - pixbrite);                                         //  colorsat is -1 ... +1
         G9 = G9 + colorsat * (G9 - pixbrite);
         B9 = B9 + colorsat * (B9 - pixbrite);
      }

      //  apply brightness/contrast curves

      if (sd->mod[0])                                                                  //  curve was modified
      {
         pixbrite = R9;                                                                //  use max. RGB value
         if (G9 > pixbrite) pixbrite = G9;
         if (B9 > pixbrite) pixbrite = B9;

         ii = coeff * pixbrite;                                                        //  "all" curve index 0-999
         if (ii < 0) ii = 0;
         if (ii > 999) ii = 999;
         ff = 2.0 * sd->yval[0][ii];                                                   //  0.0 - 2.0
         R9 = ff * R9;
         G9 = ff * G9;
         B9 = ff * B9;
      }

      if (sd->mod[1])
      {
         ii = coeff * R9;                                                              //  additional RGB curve adjustments
         if (ii < 0) ii = 0;
         if (ii > 999) ii = 999;
         R9 = R9 * (0.5 + sd->yval[1][ii]);                                            //  0.5 - 1.5
      }

      if (sd->mod[2])
      {
         ii = coeff * G9;
         if (ii < 0) ii = 0;
         if (ii > 999) ii = 999;
         G9 = G9 * (0.5 + sd->yval[2][ii]);
      }

      if (sd->mod[3])
      {
         ii = coeff * B9;
         if (ii < 0) ii = 0;
         if (ii > 999) ii = 999;
         B9 = B9 * (0.5 + sd->yval[3][ii]);
      }

      RGBfix(R9,G9,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 area
      {
         R3 = blend * R9 + (1-blend) * R1;                                             //  increase edit
         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;
}


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

//  Return relative RGB illumination values for a light source
//     having a given input temperature of 1000-10000 deg. K
//  5000 K is neutral: all returned factors relative to 1.0

void blackbodyRGB(int K, float &R, float &G, float &B)
{
   float    kk[19] = { 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0 };
   float    r1[19] = { 255, 255, 255, 255, 255, 255, 255, 255, 254, 250, 242, 231, 220, 211, 204, 197, 192, 188, 184 };
   float    g1[19] = { 060, 148, 193, 216, 232, 242, 249, 252, 254, 254, 251, 245, 239, 233, 228, 224, 220, 217, 215 };
   float    b1[19] = { 000, 010, 056, 112, 158, 192, 219, 241, 253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 };

   static int     ftf = 1;
   static float   r2[10000], g2[10000], b2[10000];

   if (ftf) {                                                                          //  initialize spline curves
      spline1(19,kk,r1);
      for (int T = 1000; T < 10000; T++)
         r2[T] = spline2(0.001 * T);

      spline1(19,kk,g1);
      for (int T = 1000; T < 10000; T++)
         g2[T] = spline2(0.001 * T);

      spline1(19,kk,b1);
      for (int T = 1000; T < 10000; T++)
         b2[T] = spline2(0.001 * T);

      ftf = 0;
   }

   if (K < 1000 || K > 9999) zappcrash("blackbody bad K: %dK",K);

   R = r2[K];
   G = g2[K];
   B = b2[K];

   return;
}


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

//  Rescale image

namespace rescale_names
{
   editfunc  EFrescale;
   int       width, height;                                                            //  new image size
   int       orgwidth, orgheight;                                                      //  original image size
   int       pctwidth, pctheight;                                                      //  percent of original size
   int       Fnewwidth, Fnewheight;                                                    //  flags, width or height was changed
   int       Flockratio;                                                               //  flag, W/H ratio is locked
   float     WHratio;
}


void m_rescale(GtkWidget *, ch *menu)
{
   using namespace rescale_names;

   int    rescale_dialog_event(zdialog *zd, ch *event);
   void * rescale_thread(void *);

   F1_help_topic = TX("RESCALE");

   printf("m_rescale \n");

   ch       rtext[12];

   EFrescale.menuname = "Rescale";
   EFrescale.menufunc = m_rescale;
   EFrescale.Fscript = 1;                                                              //  script supported
   EFrescale.threadfunc = rescale_thread;                                              //  thread function

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

/****
       __________________________________________________
      |                                                  |
      |             Pixels    Percent                    |
      |  Width     [_____]    [_____]   [Previous]       |
      |  Height    [_____]    [_____]                    |
      |                                                  |
      |  [_] 3/1  [_] 2/1  [_] 3/2  [_] 1/1              |
      |  [_] 3/4  [_] 2/3  [_] 1/2  [_] 1/3  [_] 1/4     |
      |                                                  |
      |  W/H Ratio: 1.333    Lock [_]                    |
      |                                         [OK] [X] |
      |__________________________________________________|

****/

   zdialog *zd = zdialog_new(TX("Rescale Image"),Mwin,"OK","X",null);
   EFrescale.zd = zd;
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vb11","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb12","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb13","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb14","hb1",0,"homog|space=10");
   zdialog_add_widget(zd,"label","placeholder","vb11",0);
   zdialog_add_widget(zd,"label","labw","vb11",TX("Width"));
   zdialog_add_widget(zd,"label","labh","vb11",TX("Height"));
   zdialog_add_widget(zd,"label","labpix","vb12",TX("Pixels"));
   zdialog_add_widget(zd,"zspin","width","vb12","10|80000|1|200","size=6");            //  fotocx.h limits                       26.3
   zdialog_add_widget(zd,"zspin","height","vb12","10|80000|1|200","size=6");
   zdialog_add_widget(zd,"label","labpct","vb13",TX("Percent"));
   zdialog_add_widget(zd,"zspin","pctwidth","vb13","1|999|1|100");
   zdialog_add_widget(zd,"zspin","pctheight","vb13","1|999|1|100");
   zdialog_add_widget(zd,"button","prev","vb14",TX("Previous"));

   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","space","hb2",0);
   zdialog_add_widget(zd,"check","3/1","hb2","3/1");
   zdialog_add_widget(zd,"check","2/1","hb2","2/1");
   zdialog_add_widget(zd,"check","3/2","hb2","3/2");
   zdialog_add_widget(zd,"check","1/1","hb2","1/1");

   zdialog_add_widget(zd,"hbox","hb3","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","space","hb3",0);
   zdialog_add_widget(zd,"check","3/4","hb3","3/4");
   zdialog_add_widget(zd,"check","2/3","hb3","2/3");
   zdialog_add_widget(zd,"check","1/2","hb3","1/2");
   zdialog_add_widget(zd,"check","1/3","hb3","1/3");
   zdialog_add_widget(zd,"check","1/4","hb3","1/4");

   zdialog_add_widget(zd,"hbox","hb4","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labratio","hb4",TX("W/H Ratio:"),"space=5");
   zdialog_add_widget(zd,"label","WHratio","hb4","1.6667");
   zdialog_add_widget(zd,"check","Flockratio","hb4",TX("Lock"),"space=10");

   zdialog_add_ttip(zd,"prev",TX("use previous input"));

   orgwidth = E1pxm->ww;                                                               //  original width, height
   orgheight = E1pxm->hh;
   width = orgwidth;                                                                   //  = initial width, height
   height = orgheight;
   WHratio = 1.0 * width / height;                                                     //  initial W/H ratio
   pctwidth = pctheight = 100;
   Fnewwidth = Fnewheight = 0;
   Flockratio = 1;                                                                     //  W/H ratio initially locked

   zdialog_stuff(zd,"width",width);
   zdialog_stuff(zd,"height",height);
   zdialog_stuff(zd,"pctwidth",pctwidth);
   zdialog_stuff(zd,"pctheight",pctheight);
   snprintf(rtext,12,"%.3f",WHratio);
   zdialog_stuff(zd,"WHratio",rtext);
   zdialog_stuff(zd,"Flockratio",1);

   zdialog_stuff(zd,"3/1",0);
   zdialog_stuff(zd,"2/1",0);
   zdialog_stuff(zd,"3/2",0);
   zdialog_stuff(zd,"1/1",1);                                                          //  begin with full size
   zdialog_stuff(zd,"3/4",0);
   zdialog_stuff(zd,"2/3",0);
   zdialog_stuff(zd,"1/2",0);
   zdialog_stuff(zd,"1/3",0);
   zdialog_stuff(zd,"1/4",0);

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


//  dialog event and completion callback function

int rescale_dialog_event(zdialog *zd, ch *event)
{
   using namespace rescale_names;

   int         nn;
   double      memreq, memavail;
   ch          rtext[12];
   int         pixcc = 4 * sizeof(float);                                              //  pixel size, RGBA

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

   if (strmatch(event,"apply")) {                                                      //  from script
      zdialog_fetch(zd,"width",width);
      zdialog_fetch(zd,"height",height);
      thread_signal();
      return 1;
   }

   if (zd->zstat)                                                                      //  dialog complete
   {
      if (zd->zstat == 1) {                                                            //  done
         editrescale[0] = width;                                                       //  remember size used
         editrescale[1] = height;
         thread_signal();
         edit_addhist("width:%d height:%d",width,height);                              //  edit params > edit hist
         edit_done(0);
      }
      else edit_cancel(0);                                                             //  cancel or kill
      Fzoom = 0;
      return 1;
   }

   if (strmatch(event,"focus")) return 1;                                              //  ignore focus

   Fnewwidth = Fnewheight = 0;                                                         //  reset flags

   if (strmatch(event,"pctwidth")) {                                                   //  % width input
      zdialog_fetch(zd,"pctwidth",nn);
      if (nn != pctwidth) {
         width = nn / 100.0 * orgwidth + 0.5;
         Fnewwidth = 1;                                                                //  note changed
      }
   }

   if (strmatch(event,"pctheight")) {                                                  //  same for height
      zdialog_fetch(zd,"pctheight",nn);
      if (nn != pctheight) {
         height = nn / 100.0 * orgheight + 0.5;
         Fnewheight = 1;
      }
   }

   if (strmatch(event,"width")) {                                                      //  width input
      zdialog_fetch(zd,"width",nn);
      if (nn != width) {
         width = nn;                                                                   //  set new width
         Fnewwidth = 1;                                                                //  note change
      }
   }

   if (strmatch(event,"height")) {                                                     //  same for height
      zdialog_fetch(zd,"height",nn);
      if (nn != height) {
         height = nn;
         Fnewheight = 1;
      }
   }

   if (Fnewwidth || Fnewheight) {                                                      //  width or height was set directly
      zdialog_stuff(zd,"3/1",0);                                                       //  uncheck all preset sizes
      zdialog_stuff(zd,"2/1",0);
      zdialog_stuff(zd,"3/2",0);
      zdialog_stuff(zd,"1/1",0);
      zdialog_stuff(zd,"3/4",0);
      zdialog_stuff(zd,"2/3",0);
      zdialog_stuff(zd,"1/2",0);
      zdialog_stuff(zd,"1/3",0);
      zdialog_stuff(zd,"1/4",0);
   }

   if (zstrstr("3/1 2/1 3/2 1/1 3/4 2/3 1/2 1/3 1/4",event)) {                         //  a ratio button was selected
      zdialog_stuff(zd,"3/1",0);                                                       //  uncheck all preset sizes
      zdialog_stuff(zd,"2/1",0);
      zdialog_stuff(zd,"3/2",0);
      zdialog_stuff(zd,"1/1",0);
      zdialog_stuff(zd,"3/4",0);
      zdialog_stuff(zd,"2/3",0);
      zdialog_stuff(zd,"1/2",0);
      zdialog_stuff(zd,"1/3",0);
      zdialog_stuff(zd,"1/4",0);
      zdialog_stuff(zd,event,1);                                                       //  set selected button on

      if (strmatch(event,"3/1")) {
         width = 3 * orgwidth;
         height = 3 * orgheight;
      }

      if (strmatch(event,"2/1")) {
         width = 2 * orgwidth;
         height = 2 * orgheight;
      }

      if (strmatch(event,"3/2")) {
         width = 1.5 * orgwidth;
         height = 1.5 * orgheight;
      }

      if (strmatch(event,"1/1")) {
         width = orgwidth;
         height = orgheight;
      }

      if (strmatch(event,"3/4")) {
         width = 0.75 * orgwidth;
         height = 0.75 * orgheight;
      }

      if (strmatch(event,"2/3")) {
         width = 0.66667 * orgwidth;
         height = 0.66667 * orgheight;
      }

      if (strmatch(event,"1/2")) {
         width = 0.5 * orgwidth;
         height = 0.5 * orgheight;
      }

      if (strmatch(event,"1/3")) {
         width = 0.33333 * orgwidth;
         height = 0.33333 * orgheight;
      }

      if (strmatch(event,"1/4")) {
         width = 0.25 * orgwidth;
         height = 0.25 * orgheight;
      }

      WHratio = 1.0 * orgwidth / orgheight;                                            //  new W/H ratio
      Flockratio = 1;                                                                  //  lock W/H ratio
   }

   if (strmatch("prev",event)) {                                                       //  Previous size button checked
      zdialog_stuff(zd,"3/1",0);                                                       //  uncheck all preset sizes
      zdialog_stuff(zd,"2/1",0);
      zdialog_stuff(zd,"3/2",0);
      zdialog_stuff(zd,"1/1",0);
      zdialog_stuff(zd,"3/4",0);
      zdialog_stuff(zd,"2/3",0);
      zdialog_stuff(zd,"1/2",0);
      zdialog_stuff(zd,"1/3",0);
      zdialog_stuff(zd,"1/4",0);

      width = editrescale[0];                                                          //  set previous size
      height = editrescale[1];

      WHratio = 1.0 * width / height;                                                  //  new W/H ratio
      Flockratio = 1;                                                                  //  lock W/H ratio
   }

   if (strmatch(event,"Flockratio"))                                                   //  toggle W/H ratio lock
      zdialog_fetch(zd,"Flockratio",Flockratio);

   if (Flockratio) {                                                                   //  if W/H ratio locked,
      if (Fnewwidth) height = width / WHratio + 0.5;                                   //    force same W/H ratio
      if (Fnewheight) width = height * WHratio + 0.5;                                  //  round
   }

   if (width < 10) {                                                                   //  enforce lower limits
      width = 10;
      height = width / WHratio + 0.5;
   }

   if (height < 10) {
      height = 10;
      width = height * WHratio + 0.5;
   }

   if (! edit_wwhhOK(width,height)) return 1;                                          //  enforce image size limits

   if (! Flockratio) WHratio = 1.0 * width / height;                                   //  track actual W/H ratio

   pctwidth = 100.0 * width / orgwidth + 0.5;                                          //  final % size
   pctheight = 100.0 * height / orgheight + 0.5;

   zdialog_stuff(zd,"width",width);                                                    //  update all zdialog data
   zdialog_stuff(zd,"height",height);
   zdialog_stuff(zd,"pctwidth",pctwidth);
   zdialog_stuff(zd,"pctheight",pctheight);
   zdialog_stuff(zd,"Flockratio",Flockratio);
   snprintf(rtext,12,"%.3f",WHratio);
   zdialog_stuff(zd,"WHratio",rtext);

   memreq = 1.0 * width * height * pixcc;                                              //  memory for rescaled image
   memreq = memreq / 1024 / 1024;                                                      //  peak required memory, MB
   memavail = availmemory();
   if (memreq > memavail - 1000) {                                                     //  1 GB margin
      zmessageACK(Mwin,TX("insufficient memory, cannot proceed"));
      edit_cancel(0);
      return 1;
   }

   thread_signal();                                                                    //  update image, no wait for idle
   return 1;
}


//  do the rescale job

void * rescale_thread(void *)
{
   using namespace rescale_names;

   PXM   *pxmtemp;

   pxmtemp = PXM_rescale(E1pxm,width,height);                                          //  rescale the edit image
   if (! pxmtemp) {
      zmessageACK(Mwin,TX("insufficient memory, cannot proceed"));                     //  26.3
      return 0;
   }
   PXM_free(E3pxm);
   E3pxm = pxmtemp;
   Eww = E3pxm->ww;
   Ehh = E3pxm->hh;

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

   Fpaint2();
   return 0;
}


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

//  Use the split-screen method to view before/after images during an edit function.
//  If no edit function is active, split_screen2() is started to choose two files.

namespace split_screen1_names
{
   int      P, PP, Finit;
   int      Pww, Phh, Rww;
   PXM      *E2pxm = 0;
   zdialog  *zd;
}


//  menu function

void m_split_screen1(GtkWidget *, ch *menu)                                            //  25.1
{
   using namespace split_screen1_names;

   int split_screen1_dialog_event(zdialog *, ch *);
   int split_screen1_timerfunc(void *);

   void split_screen1_mousefunc();
   void split_screen1_show();

   F1_help_topic = TX("SPLIT-SCREEN");
   printf("m_split_screen1 \n");

   if (! CEF || ! E1pxm || ! E3pxm) {                                                  //  if no edit function active,
      m_split_screen2(0,0);                                                            //   choose files to view split-screen
      return;
   }

   zd = zdialog_new(null,Mwin,"OK",null);                                              //  small dialog to exit split screen
   zdialog_add_widget(zd,"label","labx","dialog",TX("exit split screen"));
   zdialog_set_decorated(zd,0);
   zdialog_run(zd,split_screen1_dialog_event,"90/90");

   Rww = 3 / Mscale;                                                                   //  red line width
   if (Rww < 1) Rww = 1;
   Pww = E1pxm->ww;                                                                    //  image dimensions
   Phh = E1pxm->hh;
   PP = P = Pww/2;                                                                     //  initial split line

   E2pxm = PXM_copy(E3pxm);                                                            //  left/right images: E1/E2
                                                                                       //  kidnap E3 for split image
   Finit = 1;
   split_screen1_show();                                                               //  show initial split screen
   takeMouse(split_screen1_mousefunc,dragcursor);                                      //  capture mouse

   g_timeout_add(100,split_screen1_timerfunc,0);                                       //  start timer function, 100ms interval

   return;
}


//  dialog event function - kill split screen and restore edit E3 image

int split_screen1_dialog_event(zdialog *zd, ch *event)
{
   using namespace split_screen1_names;

   if (zd->zstat == 0) return 0;
   zdialog_free(zd);
   freeMouse();
   if (! CEF) return 0;
   PXM_free(E3pxm);
   E3pxm = E2pxm;
   E2pxm = 0;
   edit_undo();
   edit_redo();
   return 0;
}


//  timer function to detect when edit is done or canceled
//  so the dialog can be killed and the mouse set free

int split_screen1_timerfunc(void *)
{
   using namespace split_screen1_names;

   if (! CEF || ! E1pxm || ! E2pxm || ! E3pxm ||                                       //  detect if edit done or changed
      E1pxm->ww != Pww || E2pxm->ww != Pww || E3pxm->ww != Pww ||
      E1pxm->hh != Phh || E2pxm->hh != Phh || E3pxm->hh != Phh)
   {
      PXM_free(E2pxm);
      E2pxm = 0;
      freeMouse();
      if (zdialog_valid(zd)) zdialog_free(zd);
      return 0;                                                                        //  kill timer
   }

   return 1;                                                                           //  continue timer
}


//  mouse function - drag image separator using the mouse

void split_screen1_mousefunc()
{
   using namespace split_screen1_names;

   void split_screen1_show();

   if (! Mxdrag && ! Mydrag) return;                                                   //  wait for drag event

   P = Mxdrag;                                                                         //  mouse drag x position
   split_screen1_show();
   Mxdrag = Mydrag = 0;
   return;
}


//  show the two images with split screen
//  use pxbL image for pixels left of P and pxbR image for pixels right of P

void split_screen1_show()
{
   using namespace split_screen1_names;

   int         px, py, rx, cc;
   int         pcc = E3pxm->nc * sizeof(float);
   float       *pix1, *pix2, *pix3;
   float       red[4] = { 255, 0, 0, 255 };

   if (Finit)                                                                          //  initial 50/50 image
   {
      Finit = 0;

      for (py = 0; py < Phh; py++)
      {
         px = 0;                                                                       //  left image
         cc = P-Rww;
         if (cc > 0) {
            pix1 = PXMpix(E1pxm,px,py);
            pix3 = PXMpix(E3pxm,px,py);
            memcpy(pix3,pix1,cc*pcc);
         }

         for (px = P-Rww; px <= P+Rww; px++)                                           //  red line
         {
            if (px >= 0 && px < Pww) {
               pix3 = PXMpix(E3pxm,px,py);
               memcpy(pix3,red,pcc);
            }
         }

         px = P+Rww;                                                                   //  right image
         cc = Pww - px - 1;
         if (cc > 0) {
            pix2 = PXMpix(E2pxm,px,py);
            pix3 = PXMpix(E3pxm,px,py);
            memcpy(pix3,pix2,cc*pcc);
         }
      }

      Fpaint2();
   }

   if (P == PP) return;                                                                //  no movement

   px = cc = 0;

   for (py = 0; py < Phh; py++)
   {
      if (P > PP)                                                                      //  movement to right
      {
         px = PP - 2 * Rww;
         cc = P - PP + 4 * Rww;
         if (px < 0) px = 0;
         if (px + cc > Pww-1) cc = Pww-1 - px;
         pix1 = PXMpix(E1pxm,px,py);                                                   //  expose left image
         pix3 = PXMpix(E3pxm,px,py);
         memcpy(pix3,pix1,cc*pcc);
      }

      if (P < PP)                                                                      //  movement to left
      {
         px = P - 2 * Rww;
         cc = PP - P + 4 * Rww;
         if (px < 0) px = 0;
         if (px + cc > Pww-1) cc = Pww-1 - px;
         pix2 = PXMpix(E2pxm,px,py);                                                   //  expose right image
         pix3 = PXMpix(E3pxm,px,py);
         memcpy(pix3,pix2,cc*pcc);
      }

      for (rx = P - Rww; rx <= P + Rww; rx++)                                          //  red line
      {
         if (rx >= 0 && rx < Pww) {
            pix3 = PXMpix(E3pxm,rx,py);
            memcpy(pix3,red,pcc);
         }
      }
   }

   PP = P;                                                                             //  save prior position

   if (px < 0) px = 0;
   if (px + cc > Pww-1) cc = Pww-1 - px;

   Fpaint3(px,0,cc,Phh,0);                                                             //  paint new exposed area
   return;
}


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

//   Split Screen2 function
//   Stack two images of exactly the same size with no automatic alignment.
//   The two images are split left and right by a movable vertical boundary.

namespace split_screen2_names
{
   int         Pww, Phh, P, PP;
   int         Finit;
   PXM         *E2pxm = 0;
   editfunc    EFsplit_screen2;
}


//  menu function

void m_split_screen2(GtkWidget *, ch *menu)                                            //  25.1
{
   using namespace split_screen2_names;

   int split_screen2_dialog_event(zdialog *, ch *);
   int split_screen2_start_dialog_event(zdialog *, ch *);
   void   split_screen2_mousefunc();
   void   split_screen2_split();

   zdialog  *zd = 0;
   int      zstat, err;
   ch       *fileL, *fileR;
   PXB      *pxbL = 0, *pxbR = 0;

   printf("m_split_screen2 \n");

   if (Fblock("split_screen2")) return;
   Fblock(0);

   select_files(0);

   if (SFcount == 0) return;

   if (SFcount != 2) {
      zmessageACK(Mwin,TX("select two files to compare"));
      return;
   }

   fileL = zstrdup(SelFiles[0],"split_screen2");
   fileR = zstrdup(SelFiles[1],"split_screen2");

   if (image_file_type(fileL) != IMAGE && image_file_type(fileL) != RAW) {
      zmessageACK(Mwin,TX("left file is not an image file"));
      return;
   }

   if (image_file_type(fileR) != IMAGE && image_file_type(fileR) != RAW) {
      zmessageACK(Mwin,TX("right file is not an image file"));
      return;
   }

   pxbL = PXB_load(fileL,1);
   if (! pxbL) {
      zmessageACK(Mwin,TX("cannot load the left file"));
      return;
   }

   pxbR = PXB_load(fileR,1);
   if (! pxbR) {
      zmessageACK(Mwin,TX("cannot load the right file"));
      return;
   }

   if (pxbL->ww != pxbR->ww || pxbL->hh != pxbR->hh) {
      zmessageACK(Mwin,TX("images are not the same dimensions"));
      return;
   }

   Pww = pxbL->ww;
   Phh = pxbL->hh;

   PXB_free(pxbL);
   PXB_free(pxbR);
   pxbL = pxbR = 0;

   err = f_open(fileL);                                                                //  left file is the one to edit
   if (err) return;                                                                    //  (becomes E1pxm)

   EFsplit_screen2.menufunc = m_split_screen2;                                         //  fake edit function
   EFsplit_screen2.menuname = "Split Screen2";
   EFsplit_screen2.mousefunc = split_screen2_mousefunc;
   if (! edit_setup(EFsplit_screen2)) return;

   E2pxm = PXM_load(fileR,1);                                                          //  E1/E2 = left/right file

   Finit = 1;                                                                          //  initial 50/50 split
   split_screen2_split();

   takeMouse(split_screen2_mousefunc,dragcursor);                                      //  connect mouse function

   zd = zdialog_new(null,Mwin,"keep","discard",null);                                  //  popup window for completion buttons
   zdialog_add_widget(zd,"label","labtip","dialog",TX("drag image boundary"));         //    [keep]  [discard]
   zdialog_set_decorated(zd,0);

   zdialog_run(zd,split_screen2_start_dialog_event,"save");                            //  run dialog, parallel
   zstat = zdialog_wait(zd);                                                           //  wait for completion
   zdialog_free(zd);

   PXM_free(E2pxm);
   E2pxm = 0;

   if (zstat == 1) {                                                                   //  [OK]
      CEF->Fmods++;
      CEF->Fsaved = 0;
      edit_done(0);
   }
   else edit_cancel(0);                                                                //  [X]

   return;
}


//  get files dialog event and completion function

int split_screen2_dialog_event(zdialog *zd, ch *event)
{
   using namespace split_screen2_names;

   ch          *file = 0;
   static ch   *pfile = 0;

   if (strmatch(event,"escape")) zd->zstat = -1;
   if (strmatch(event,"focus")) return 1;

   if (zd->zstat) return 1;                                                            //  OK or cancel

   if (! pfile && curr_file)                                                           //  if no prior file,
      pfile = zstrdup(curr_file,"split_screen2");                                      //    start with current file

   if (strmatch(event,"browseL")) {
      file = zgetfile(TX("select file"),MWIN,"file",pfile);
      if (file) zdialog_stuff(zd,"fileL",file);
   }

   if (strmatch(event,"browseR")) {
      file = zgetfile(TX("select file"),MWIN,"file",pfile);
      if (file) zdialog_stuff(zd,"fileR",file);
   }

   if (file) {                                                                         //  remember last file selected
      if (pfile) zfree(pfile);
      pfile = file;
      file = 0;
   }

   return 1;
}


//  split-screen dialog event and completion callback function

int split_screen2_start_dialog_event(zdialog *zd, ch *event)
{
   using namespace split_screen2_names;

   void   split_screen2_mousefunc();

   if (zd->zstat) freeMouse();                                                         //  [OK] or [X]

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

   return 1;
}


//  split-screen mouse function

void split_screen2_mousefunc()
{
   using namespace split_screen2_names;

   void   split_screen2_split();

   if (! Mxdrag && ! Mydrag) return;                                                   //  wait for drag event

   P = Mxdrag;                                                                         //  mouse drag x position, 0...Pww-1
   split_screen2_split();

   Mxdrag = Mydrag = 0;
   return;
}


//  output revised E3 image
//  use input image 0 for pixels left of P and image 1 for pixels right of P

void split_screen2_split()
{
   using namespace split_screen2_names;

   float       *pix3, *pix1;
   int         px, py, rx, cc;
   int         pcc = E3pxm->nc * sizeof(float);
   float       red[4] = { 255, 0, 0, 255 };
   int         Rww;                                                                    //  1/2 red line width

   Rww = 3 / Mscale;
   if (Rww < 1) Rww = 1;

   if (Finit)                                                                          //  initial 50/50 image
   {
      Finit = 0;
      P = PP = Pww / 2;                                                                //  50/50 split

      for (py = 0; py < Phh; py++)
      {
         px = 0;                                                                       //  left image
         cc = P-Rww;
         if (cc > 0) {
            pix1 = PXMpix(E1pxm,px,py);
            pix3 = PXMpix(E3pxm,px,py);
            memcpy(pix3,pix1, cc * pcc);
         }

         for (px = P-Rww; px <= P+Rww; px++)                                           //  red line
         {
            if (px >= 0 && px < Pww) {
               pix3 = PXMpix(E3pxm,px,py);
               memcpy(pix3,red,pcc);
            }
         }

         px = P+Rww;                                                                   //  right image
         cc = Pww - px - 1;
         if (cc > 0) {
            pix1 = PXMpix(E2pxm,px,py);
            pix3 = PXMpix(E3pxm,px,py);
            memcpy(pix3,pix1, cc * pcc);
         }
      }

      Fpaint2();
      return;
   }

   if (P == PP) return;                                                                //  no movement

   px = cc = 0;

   for (py = 0; py < Phh; py++)
   {
      if (P > PP)                                                                      //  movement to right
      {
         px = PP - 2 * Rww;
         cc = P - PP + 4 * Rww;
         if (px < 0) px = 0;
         if (px + cc > Pww-1) cc = Pww-1 - px;
         pix1 = PXMpix(E1pxm,px,py);                                                   //  expose left image
         pix3 = PXMpix(E3pxm,px,py);
         memcpy(pix3,pix1, cc * pcc);
      }

      if (P < PP)                                                                      //  movement to left
      {
         px = P - 2 * Rww;
         cc = PP - P + 4 * Rww;
         if (px < 0) px = 0;
         if (px + cc > Pww-1) cc = Pww-1 - px;
         pix1 = PXMpix(E2pxm,px,py);                                                   //  expose right image
         pix3 = PXMpix(E3pxm,px,py);
         memcpy(pix3,pix1, cc * pcc);
      }

      for (rx = P - Rww; rx <= P + Rww; rx++)                                          //  red line
      {
         if (rx >= 0 && rx < Pww) {
            pix3 = PXMpix(E3pxm,rx,py);
            memcpy(pix3,red,pcc);
         }
      }
   }

   PP = P;                                                                             //  save prior position

   if (px < 0) px = 0;                                                                 //  check exposed area
   if (px + cc > Pww-1) cc = Pww-1 - px;
   Fpaint3(px,0,cc,Phh,0);                                                             //  paint new exposed area

   return;
}


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

//  Add Margins to an image

namespace margins_names
{
   editfunc  EFmargins;
   int       Eww1, Ehh1;
   int       Eww2, Ehh2;
   int       top, bott, left, right;
   int       red, green, blue, alpha;
}


void m_margins(GtkWidget *, ch *menu)
{
   using namespace margins_names;

   int    margins_dialog_event(zdialog *zd, ch *event);
   void * margins_thread(void *);

   F1_help_topic = TX("MARGINS");

   printf("m_margins \n");

   EFmargins.menuname = TX("Margins");
   EFmargins.menufunc = m_margins;
   EFmargins.FprevReq = 1;                                                             //  use preview (small) size
   EFmargins.Fscript = 1;                                                              //  script supported
   EFmargins.threadfunc = margins_thread;                                              //  thread function

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

   Eww1 = E1pxm->ww;                                                                   //  input image dimensions
   Ehh1 = E1pxm->hh;

   PXM_addalpha(E1pxm);

/****
          ________________________________
         |         Add Margins            |
         |                                |
         |    Top   [____]  All ± 1  [__] |
         |    Left  [____]  All ± 10 [__] |
         |   Right  [____]   Color  [###] |
         |   Bottom [____]   Alpha  [___] |
         |                                |
         |                       [OK] [X] |
         |________________________________|

****/

   zdialog *zd = zdialog_new(TX("Add Margins"),Mwin,"OK","X",null);
   EFmargins.zd = zd;
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"label","space","hb1","   ");
   zdialog_add_widget(zd,"vbox","vb3","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb4","hb1",0,"homog|space=5");

   zdialog_add_widget(zd,"label","Ltop","vb1",TX("Top"));
   zdialog_add_widget(zd,"label","Lleft","vb1",TX("Left"));
   zdialog_add_widget(zd,"label","Lright","vb1",TX("Right"));
   zdialog_add_widget(zd,"label","Lbott","vb1",TX("Bottom"));

   zdialog_add_widget(zd,"zspin","top","vb2","0|999|1|0");
   zdialog_add_widget(zd,"zspin","left","vb2","0|999|1|0");
   zdialog_add_widget(zd,"zspin","right","vb2","0|999|1|0");
   zdialog_add_widget(zd,"zspin","bott","vb2","0|999|1|0");

   zdialog_add_widget(zd,"label","Lall1","vb3",TX("All±1"));
   zdialog_add_widget(zd,"label","Lall10","vb3",TX("All±10"));
   zdialog_add_widget(zd,"label","Lcolor","vb3",TX("Color"));
   zdialog_add_widget(zd,"label","Lalpha","vb3",TX("Alpha"));

   zdialog_add_widget(zd,"zspin","all±1","vb4","-1|+1|1|0");
   zdialog_add_widget(zd,"zspin","all±10","vb4","-10|+10|10|0");
   zdialog_add_widget(zd,"colorbutt","color","vb4","255|255|255");
   zdialog_add_widget(zd,"zspin","alpha","vb4","0|255|1|255");

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


//  dialog event and completion callback function

int margins_dialog_event(zdialog *zd, ch *event)
{
   using namespace margins_names;

   void  margins_final();

   int      addall;
   ch       color[20];
   ch       *pp;

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

   if (zd->zstat)                                                                      //  dialog complete
   {
      if (zd->zstat == 1)                                                              //  done
      {
         edit_fullsize();                                                              //  get full size image
         PXM_addalpha(E1pxm);
         margins_final();                                                              //  rotate/crop final image
         edit_addhist("%d %d %d %d",top,bott,left,right);                              //  edit params > edit hist
         edit_done(0);
      }
      else edit_cancel(0);                                                             //  cancel or kill
      return 1;
   }

   if (strmatch(event,"all±1")) {
      zdialog_fetch(zd,"all±1",addall);
      zdialog_stuff(zd,"all±1",0);
   }

   if (strmatch(event,"all±10")) {
      zdialog_fetch(zd,"all±10",addall);
      zdialog_stuff(zd,"all±10",0);
   }

   if (addall)
   {
      zdialog_fetch(zd,"top",top);
      top += addall;
      zdialog_stuff(zd,"top",top);

      zdialog_fetch(zd,"bott",bott);
      bott += addall;
      zdialog_stuff(zd,"bott",bott);

      zdialog_fetch(zd,"left",left);
      left += addall;
      zdialog_stuff(zd,"left",left);

      zdialog_fetch(zd,"right",right);
      right += addall;
      zdialog_stuff(zd,"right",right);

      addall = 0;
   }

   zdialog_fetch(zd,"top",top);                                                        //  get margin sizes
   zdialog_fetch(zd,"bott",bott);
   zdialog_fetch(zd,"left",left);
   zdialog_fetch(zd,"right",right);
   zdialog_fetch(zd,"alpha",alpha);                                                    //  margin alpha channel

   zdialog_fetch(zd,"color",color,20);                                                 //  margin color from color button
   pp = substring(color,"|",1);
   if (pp) red = atoi(pp);
   pp = substring(color,"|",2);
   if (pp) green = atoi(pp);
   pp = substring(color,"|",3);
   if (pp) blue = atoi(pp);

   thread_signal();                                                                    //  update image, no wait
   return 1;
}


//  do the margins job

void * margins_thread(void *)
{
   using namespace margins_names;

   PXM      *pxmtemp;
   int      leftx, topx;
   int      cc, ii;
   float    pix1[4], *pix2;

   topx = top;                                                                         //  guaranteed constant values
   leftx = left;

   pix1[0] = red;                                                                      //  margin color and alpha
   pix1[1] = green;
   pix1[2] = blue;
   pix1[3] = alpha;

   Eww2 = Eww1 + right + leftx;                                                        //  output image size
   Ehh2 = Ehh1 + topx + bott;

   pxmtemp = PXM_make(Eww2,Ehh2,4);                                                    //  output image with alpha channel
   pix2 = pxmtemp->pixels;
   cc = 4 * sizeof(float);

   for (ii = 0; ii < Eww2 * Ehh2 * 4; ii += 4)                                         //  fill output image with margin color
      memcpy(pix2+ii,pix1,cc);

   PXM_copy_area(E1pxm,pxmtemp,0,0,leftx,topx,Eww1,Ehh1);                              //  copy input image into output
   PXM_free(E3pxm);
   E3pxm = pxmtemp;
   Eww = E3pxm->ww;
   Ehh = E3pxm->hh;

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

   Fpaint2();
   return 0;
}


void margins_final()
{
   using namespace margins_names;

   PXM      *pxmtemp;
   int      cc, ii;
   float    pix1[4], *pix2;
   float    R;

   pix1[0] = red;                                                                      //  margin color and alpha
   pix1[1] = green;
   pix1[2] = blue;
   pix1[3] = alpha;

   if (E1pxm->ww == Eww1) return;                                                      //  preview image is full size image

   R = 1.0 * (Eww2 + Ehh2) / (Eww1 + Ehh1);                                            //  size with margins / size without

   top *= R + 0.5;                                                                     //  rescale full size margins
   bott *= R + 0.5;
   left *= R + 0.5;
   right *= R + 0.5;

   Eww1 = E1pxm->ww;                                                                   //  full size input image
   Ehh1 = E1pxm->hh;

   Eww2 = Eww1 + right + left;                                                         //  full size output image
   Ehh2 = Ehh1 + top + bott;

   pxmtemp = PXM_make(Eww2,Ehh2,4);                                                    //  output image with alpha channel
   pix2 = pxmtemp->pixels;
   cc = 4 * sizeof(float);

   for (ii = 0; ii < Eww2 * Ehh2 * 4; ii += 4)                                         //  fill output image with margin color
      memcpy(pix2+ii,pix1,cc);

   PXM_copy_area(E1pxm,pxmtemp,0,0,left,top,Eww1,Ehh1);                                //  copy input image into output
   PXM_free(E3pxm);
   E3pxm = pxmtemp;
   Eww = E3pxm->ww;
   Ehh = E3pxm->hh;

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

   Fpaint2();

   return;
}


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

//  draw on image: text, line/arrow, box, oval

void m_markup(GtkWidget *widget, ch *menu)
{
   int  markup_dialog_event(zdialog *zd, ch *event);

   zdialog  *zd;

   F1_help_topic = TX("MARKUP");

   printf("m_markup \n");

/***
       ___________________________________
      |           Image Markup            |
      |                                   |
      |  [_] Draw text on image           |
      |  [_] Draw line or arrow on image  |
      |  [_] Draw box on image            |
      |  [_] Draw oval on image           |
      |                                   |
      |                               [X] |
      |___________________________________|

***/

   zd = zdialog_new(TX("Image Markup"),Mwin,"X",null);
   zdialog_add_widget(zd,"check","text","dialog",TX("Draw text on image"));
   zdialog_add_widget(zd,"check","line","dialog",TX("Draw line or arrow on image"));
   zdialog_add_widget(zd,"check","box","dialog",TX("Draw box on image"));
   zdialog_add_widget(zd,"check","oval","dialog",TX("Draw oval on image"));

   zdialog_stuff(zd,"text",0);
   zdialog_stuff(zd,"line",0);
   zdialog_stuff(zd,"box",0);
   zdialog_stuff(zd,"oval",0);

   zdialog_run(zd,markup_dialog_event,"save");
   return;
}


//  dialog event and completion function

int markup_dialog_event(zdialog *zd, ch *event)
{
   if (zd->zstat) {
      zdialog_free(zd);
      return 1;
   }

   if (strmatch(event,"text")) m_draw_text(0,0);
   if (strmatch(event,"line")) m_draw_line(0,0);
   if (strmatch(event,"box")) m_draw_box(0,0);
   if (strmatch(event,"oval")) m_draw_oval(0,0);

   if (zstrstr("text line box oval",event)) zdialog_free(zd);

   return 1;
}


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

//  add text on top of the image

namespace drawtext_names
{
   textattr_t  attr;                                                                   //  text attributes and image

   ch       file[1000] = "";                                                           //  file for write_text data
   ch       metatag[60] = "";
   int      Tpx, Tpy;                                                                  //  text position on image
   int      textpresent;                                                               //  flag, text present on image

   int   dialog_event(zdialog *zd, ch *event);                                         //  dialog event function
   void  mousefunc();                                                                  //  mouse event function
   void  write(int mode);                                                              //  write text on image

   editfunc    EFdrawtext;
}


//  menu function

void m_draw_text(GtkWidget *, ch *menu)
{
   using namespace drawtext_names;

   ch      *title = TX("Draw text on image");
   ch      *tip = TX("Enter text, click/drag on image, right click to remove");

   printf("m_draw_text \n");

   EFdrawtext.menufunc = m_draw_text;
   EFdrawtext.menuname = "Markup Text";
   EFdrawtext.Farea = 1;                                                               //  select area ignored
   EFdrawtext.mousefunc = mousefunc;

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

/***
       _________________________________________________________________
      |                        Draw text on image                       |
      |                                                                 |
      |     Enter text, click/drag on image, right click to remove.     |
      |                                                                 |
      |  Text [_______________________________________________________] |        text
      |  Use metadata tag [___________________________________] [Fetch] |        metatag "Fetch"
      |  [Font] [FreeSans_________]  Size [ 44|v]                       |        "Font" fontname fontsize
      |                                                                 |
      |           Color  Transp. Width  Angle                           |
      |  text     [###]  [____]         [____]                          |        txcolor txtransp txangle
      |  backing  [###]  [____]                                         |        bgcolor bgtransp
      |  outline  [###]  [____]  [___]                                  |        tocolor totransp towidth
      |  shadow   [###]  [____]  [___]  [____]                          |        shcolor shtransp shwidth shangle
      |                                                                 |
      |  emboss radius [____]  depth [____]                             |
      |                                                                 |
      |  Use settings file  [Open] [Save]                               |        "Open" "Save"
      |                                                                 |
      |                                            [Clear] [Commit] [X] |
      |_________________________________________________________________|

      [Clear]     clear text and metadata tag
      [Commit]    save edit, restart dialog
      [X]         edit_cancel

***/

   zdialog *zd = zdialog_new(title,Mwin,"Clear","Commit","X",null);
   EFdrawtext.zd = zd;
   EFdrawtext.mousefunc = mousefunc;
   EFdrawtext.menufunc = m_draw_text;                                                  //  allow restart

   zdialog_add_widget(zd,"label","tip","dialog",tip,"space=5");

   zdialog_add_widget(zd,"hbox","hbtext","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labtext","hbtext","Text","space=5");
   zdialog_add_widget(zd,"zedit","text","hbtext","text","expand|wrap");

   zdialog_add_widget(zd,"hbox","hbmeta","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labmeta","hbmeta",TX("Use metadata tag"),"space=5");
   zdialog_add_widget(zd,"zentry","metatag","hbmeta",0,"space=2|expand");
   zdialog_add_widget(zd,"button","Fetch","hbmeta",TX("Fetch"));

   zdialog_add_widget(zd,"hbox","hbfont","dialog",0,"space=2");
   zdialog_add_widget(zd,"button","Font","hbfont",TX("Font"));
   zdialog_add_widget(zd,"zentry","fontname","hbfont","FreeSans","space=2|size=20");
   zdialog_add_widget(zd,"label","space","hbfont",0,"space=10");
   zdialog_add_widget(zd,"label","labfsize","hbfont",TX("Size"));
   zdialog_add_widget(zd,"zspin","fontsize","hbfont","8|500|1|40","space=3");

   zdialog_add_widget(zd,"hsep","sep1","dialog",0,"space=5");

   zdialog_add_widget(zd,"hbox","hbattr","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbattr1","hbattr",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbattr2","hbattr",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbattr3","hbattr",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbattr4","hbattr",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbattr5","hbattr",0,"homog|space=5");

   zdialog_add_widget(zd,"label","space","vbattr1");
   zdialog_add_widget(zd,"label","labtext","vbattr1",TX("text"));
   zdialog_add_widget(zd,"label","labback","vbattr1",TX("backing"));
   zdialog_add_widget(zd,"label","laboutln","vbattr1",TX("outline"));
   zdialog_add_widget(zd,"label","labshadow","vbattr1",TX("shadow"));

   zdialog_add_widget(zd,"label","labcol","vbattr2",TX("Color"));
   zdialog_add_widget(zd,"colorbutt","txcolor","vbattr2","0|0|0");
   zdialog_add_widget(zd,"colorbutt","bgcolor","vbattr2","255|255|255");
   zdialog_add_widget(zd,"colorbutt","tocolor","vbattr2","255|0|0");
   zdialog_add_widget(zd,"colorbutt","shcolor","vbattr2","255|0|0");

   zdialog_add_widget(zd,"label","labtran","vbattr3",TX("Transp."));
   zdialog_add_widget(zd,"zspin","txtransp","vbattr3","0|100|1|0");
   zdialog_add_widget(zd,"zspin","bgtransp","vbattr3","0|100|1|0");
   zdialog_add_widget(zd,"zspin","totransp","vbattr3","0|100|1|0");
   zdialog_add_widget(zd,"zspin","shtransp","vbattr3","0|100|1|0");

   zdialog_add_widget(zd,"label","labw","vbattr4",TX("Width"));
   zdialog_add_widget(zd,"label","space","vbattr4");
   zdialog_add_widget(zd,"label","space","vbattr4");
   zdialog_add_widget(zd,"zspin","towidth","vbattr4","0|30|1|0");
   zdialog_add_widget(zd,"zspin","shwidth","vbattr4","0|50|1|0");

   zdialog_add_widget(zd,"label","labw","vbattr5",TX("Angle"));
   zdialog_add_widget(zd,"zspin","txangle","vbattr5","-360|360|0.5|0");
   zdialog_add_widget(zd,"label","space","vbattr5");
   zdialog_add_widget(zd,"label","space","vbattr5");
   zdialog_add_widget(zd,"zspin","shangle","vbattr5","-360|360|1|0");

   zdialog_add_widget(zd,"hsep","sep2","dialog",0,"space=5");

   zdialog_add_widget(zd,"hbox","hbemboss","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labemboss","hbemboss",TX("emboss"),"space=5");
   zdialog_add_widget(zd,"label","labradius","hbemboss",TX("radius"),"space=5");
   zdialog_add_widget(zd,"zspin","radius","hbemboss","0|10|1|0");
   zdialog_add_widget(zd,"label","labdepth","hbemboss",TX("depth"),"space=5");
   zdialog_add_widget(zd,"zspin","depth","hbemboss","0|20|1|0");

   zdialog_add_widget(zd,"hbox","hbfile","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labfile","hbfile",TX("Use settings file"),"space=5");
   zdialog_add_widget(zd,"button","Open","hbfile","Open","space=5");
   zdialog_add_widget(zd,"button","Save","hbfile","Save","space=5");

   zdialog_add_ttip(zd,"Clear",TX("clear text"));
   zdialog_add_ttip(zd,"Commit",TX("save edit, start new text"));                      //  26.0

   zdialog_load_inputs(zd);                                                            //  restore prior inputs

   memset(&attr,0,sizeof(attr));

   zdialog_fetch(zd,"text",attr.text,1000);                                            //  get defaults or prior inputs
   zdialog_fetch(zd,"fontname",attr.font,80);
   zdialog_fetch(zd,"fontsize",attr.size);
   zdialog_fetch(zd,"txcolor",attr.color[0],20);
   zdialog_fetch(zd,"txtransp",attr.transp[0]);
   zdialog_fetch(zd,"txangle",attr.angle);
   zdialog_fetch(zd,"bgcolor",attr.color[1],20);
   zdialog_fetch(zd,"bgtransp",attr.transp[1]);
   zdialog_fetch(zd,"tocolor",attr.color[2],20);
   zdialog_fetch(zd,"totransp",attr.transp[2]);
   zdialog_fetch(zd,"towidth",attr.towidth);
   zdialog_fetch(zd,"shcolor",attr.color[3],20);
   zdialog_fetch(zd,"shtransp",attr.transp[3]);
   zdialog_fetch(zd,"shwidth",attr.shwidth);
   zdialog_fetch(zd,"shangle",attr.shangle);
   zdialog_fetch(zd,"radius",attr.emboss[0]);
   zdialog_fetch(zd,"depth",attr.emboss[1]);
   zdialog_fetch(zd,"metatag",metatag,60);

   gentext(&attr);                                                                     //  initial text

   takeMouse(mousefunc,dragcursor);                                                    //  connect mouse function
   textpresent = 0;                                                                    //  no text on image yet
   zdialog_run(zd,dialog_event,"save");                                                //  run dialog, parallel
   if (*metatag) zdialog_send_event(zd,"Fetch");                                       //  metadata tag active, get text
   return;
}


//  dialog event and completion callback function

int drawtext_names::dialog_event(zdialog *zd, ch *event)
{
   using namespace drawtext_names;

   GtkWidget   *font_dialog;
   ch          font[100];                                                              //  font name and size
   int         size, yn;
   ch          *pp;
   ch          *tagname[1];
   ch          *tagval[1];

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                            //  [clear]
         *attr.text = 0;
         zdialog_stuff(zd,"text","");
         zd->zstat = 0;                                                                //  keep dialog active
         write(2);                                                                     //  erase text on image
         return 1;
      }

      if (zd->zstat == 2) {                                                            //  [Commit]
         if (textpresent) edit_done(0);                                                //  save mods
         else edit_cancel(0);
         m_draw_text(0,0);                                                             //  restart text dialog
         return 1;
      }

      if (textpresent) {                                                               //  [X] cancel
         yn = zmessageYN(Mwin,TX("discard changes?"));                                 //  unsaved changes
         if (! yn) {
            zd->zstat = 0;                                                             //  back out
            return 1;
         }
      }

      edit_cancel(0);
      m_markup(0,0);                                                                   //  restart main dialog
      return 1;
   }

   if (strmatch(event,"focus")) {                                                      //  toggle mouse capture
      takeMouse(mousefunc,dragcursor);                                                 //  connect mouse function
      return 1;
   }

   if (strmatch(event,"Open"))                                                         //  load zdialog fields from a file
   {
      load_text(zd);
      zdialog_fetch(zd,"text",attr.text,1000);                                         //  get all zdialog fields
      zdialog_fetch(zd,"fontname",attr.font,80);
      zdialog_fetch(zd,"fontsize",attr.size);
      zdialog_fetch(zd,"txcolor",attr.color[0],20);
      zdialog_fetch(zd,"txtransp",attr.transp[0]);
      zdialog_fetch(zd,"txangle",attr.angle);
      zdialog_fetch(zd,"bgcolor",attr.color[1],20);
      zdialog_fetch(zd,"bgtransp",attr.transp[1]);
      zdialog_fetch(zd,"tocolor",attr.color[2],20);
      zdialog_fetch(zd,"totransp",attr.transp[2]);
      zdialog_fetch(zd,"towidth",attr.towidth);
      zdialog_fetch(zd,"shcolor",attr.color[3],20);
      zdialog_fetch(zd,"shtransp",attr.transp[3]);
      zdialog_fetch(zd,"shwidth",attr.shwidth);
      zdialog_fetch(zd,"shangle",attr.shangle);
      zdialog_fetch(zd,"radius",attr.emboss[0]);
      zdialog_fetch(zd,"depth",attr.emboss[1]);
   }

   if (strmatch(event,"Save")) {                                                       //  save zdialog fields to file
      save_text(zd);
      return 1;
   }

   if (strmatch(event,"Fetch")) {                                                      //  load text from metadata tagname
      zdialog_fetch(zd,"metatag",metatag,60);
      if (*metatag < ' ') return 1;
      tagname[0] = metatag;
      meta_get(curr_file,tagname,tagval,1);
      if (! tagval[0]) return 1;
      if (strlen(tagval[0]) > 999) tagval[0][999] = 0;
      repl_1str(tagval[0],attr.text,999,"\\n","\n");                                   //  replace "\n" with newlines
      zfree(tagval[0]);
      zdialog_stuff(zd,"text",attr.text);                                              //  stuff dialog with metadata
   }

   if (strmatch(event,"text"))                                                         //  get text from dialog
      zdialog_fetch(zd,"text",attr.text,1000);

   if (strmatch(event,"Font")) {                                                       //  select new font
      snprintf(font,100,"%s %d",attr.font,attr.size);
      font_dialog = gtk_font_chooser_dialog_new("select font",MWIN);
      gtk_font_chooser_set_font(GTK_FONT_CHOOSER(font_dialog),font);
      gtk_dialog_run(GTK_DIALOG(font_dialog));
      pp = gtk_font_chooser_get_font(GTK_FONT_CHOOSER(font_dialog));
      gtk_widget_destroy(font_dialog);

      if (pp) {                                                                        //  should have "fontname nn"
         strncpy0(font,pp,100);
         g_free(pp);
         pp = font + strlen(font);
         while (*pp != ' ') pp--;
         if (pp > font) {
            size = atoi(pp);
            if (size < 8) size = 8;
            zdialog_stuff(zd,"fontsize",size);
            attr.size = size;
            *pp = 0;
            strncpy0(attr.font,font,80);                                               //  get fontname = new font name
            zdialog_stuff(zd,"fontname",font);
         }
      }
   }

   if (strmatch(event,"fontsize"))                                                     //  new font size
      zdialog_fetch(zd,"fontsize",attr.size);

   if (strmatch(event,"txangle"))
      zdialog_fetch(zd,"txangle",attr.angle);

   if (strmatch(event,"txcolor"))                                                      //  foreground (text) color
      zdialog_fetch(zd,"txcolor",attr.color[0],20);

   if (strmatch(event,"bgcolor"))                                                      //  background color
      zdialog_fetch(zd,"bgcolor",attr.color[1],20);

   if (strmatch(event,"tocolor"))                                                      //  text outline color
      zdialog_fetch(zd,"tocolor",attr.color[2],20);

   if (strmatch(event,"shcolor"))                                                      //  text shadow color
      zdialog_fetch(zd,"shcolor",attr.color[3],20);

   if (strmatch(event,"txtransp"))                                                     //  foreground transparency
      zdialog_fetch(zd,"txtransp",attr.transp[0]);

   if (strmatch(event,"bgtransp"))                                                     //  background transparency
      zdialog_fetch(zd,"bgtransp",attr.transp[1]);

   if (strmatch(event,"totransp"))                                                     //  text outline transparency
      zdialog_fetch(zd,"totransp",attr.transp[2]);

   if (strmatch(event,"shtransp"))                                                     //  text shadow transparency
      zdialog_fetch(zd,"shtransp",attr.transp[3]);

   if (strmatch(event,"towidth"))                                                      //  text outline width
      zdialog_fetch(zd,"towidth",attr.towidth);

   if (strmatch(event,"shwidth"))                                                      //  text shadow width
      zdialog_fetch(zd,"shwidth",attr.shwidth);

   if (strmatch(event,"shangle"))                                                      //  text shadow angle
      zdialog_fetch(zd,"shangle",attr.shangle);

   if (strmatch(event,"radius"))                                                       //  text emboss radius
      zdialog_fetch(zd,"radius",attr.emboss[0]);

   if (strmatch(event,"depth"))                                                        //  text emboss depth
      zdialog_fetch(zd,"depth",attr.emboss[1]);

   gentext(&attr);                                                                     //  build text image from text and attributes

   if (textpresent) write(1);                                                          //  update text on image
   return 1;
}


//  mouse function, set position for text on image

void drawtext_names::mousefunc()
{
   using namespace drawtext_names;

   if (LMclick) {                                                                      //  left mouse click
      Tpx = Mxclick;                                                                   //  new text position on image
      Tpy = Myclick;
      write(1);                                                                        //  erase old, write new text on image
   }

   if (RMclick) {                                                                      //  right mouse click
      write(2);                                                                        //  erase old text, if any
      Tpx = Tpy = -1;
   }

   if (Mxdrag || Mydrag)                                                               //  mouse dragged
   {
      Tpx = Mxdrag;                                                                    //  new text position on image
      Tpy = Mydrag;
      write(1);                                                                        //  erase old, write new text on image
   }

   LMclick = RMclick = Mxdrag = Mydrag = 0;
   return;
}


//  write text on image at designated location
//  mode: 1  erase old and write to new position
//        2  erase old and write nothing

void drawtext_names::write(int mode)
{
   using namespace drawtext_names;

   if (! E1pxm) return;                                                                //  can happen with wild key bashing

   float       *pix1, *pix3;
   uint8       *pixT;
   int         px1, py1, px3, py3, done;
   float       e3part, Ot, Om, Ob;
   static int  orgx1, orgy1, ww1, hh1;                                                 //  old text image overlap rectangle
   int         orgx2, orgy2, ww2, hh2;                                                 //  new overlap rectangle
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);
   int         rad, depth, dx, dy, rgb;
   float       kernel[23][23];                                                         //  emboss radius <= 10
   float       R, sumpix,*pixN;
   PXM         *E9pxm;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   if (textpresent)
   {
      for (py3 = orgy1; py3 < orgy1 + hh1; py3++)                                      //  erase prior text image
      for (px3 = orgx1; px3 < orgx1 + ww1; px3++)                                      //  replace E3 pixels with E1 pixels
      {                                                                                //    in prior overlap rectangle
         if (px3 < 0 || px3 >= Eww) continue;
         if (py3 < 0 || py3 >= Ehh) continue;
         pix1 = PXMpix(E1pxm,px3,py3);
         pix3 = PXMpix(E3pxm,px3,py3);
         memcpy(pix3,pix1,pcc);
      }
   }

   done = 0;
   if (mode == 2) done = 1;                                                            //  erase only
   if (! *attr.text) done = 2;                                                         //  no text defined
   if (Tpx < 0 && Tpy < 0) done = 3;                                                   //  no position defined

   if (done) {
      if (textpresent) {
         Fpaint3(orgx1,orgy1,ww1,hh1,cr);                                              //  update window to erase old text
         textpresent = 0;                                                              //  mark no text present
         CEF->Fmods--;
      }

      draw_context_destroy(draw_context);
      return;
   }

   ww2 = attr.pxb_text->ww;                                                            //  text image size
   hh2 = attr.pxb_text->hh;

   if (Tpx >= Eww) Tpx = Eww-1;                                                        //  if off image, pull back within
   if (Tpy >= Ehh) Tpy = Ehh-1;

   orgx2 = Tpx - ww2/2;                                                                //  copy-to image3 location
   orgy2 = Tpy - hh2/2;

   if (orgx2 < 0) orgx2 = 0;
   if (orgy2 < 0) orgy2 = 0;
   if (orgx2 + ww2 > Eww) ww2 = Eww - orgx2;
   if (orgy2 + hh2 > Ehh) hh2 = Ehh - orgy2;

   for (py1 = 0; py1 < hh2; py1++)                                                     //  loop all pixels in text image
   for (px1 = 0; px1 < ww2; px1++)
   {
      px3 = orgx2 + px1;                                                               //  copy-to image3 pixel
      py3 = orgy2 + py1;

      if (px3 < 0 || px3 >= Eww) continue;                                             //  omit parts beyond edges
      if (py3 < 0 || py3 >= Ehh) continue;

      pixT = PXBpix(attr.pxb_text,px1,py1);                                            //  copy-from text pixel
      pix3 = PXMpix(E3pxm,px3,py3);                                                    //  copy-to image pixel

      e3part = pixT[3] / 256.0;                                                        //  text image transparency

      pix3[0] = pixT[0] + e3part * pix3[0];                                            //  combine text part + image part
      pix3[1] = pixT[1] + e3part * pix3[1];
      pix3[2] = pixT[2] + e3part * pix3[2];

      if (nc > 3) {
         Ot = (1.0 - e3part);                                                          //  text opacity
         Om = pix3[3] / 256.0;                                                         //  image opacity
         Ob = 1.0 - (1.0 - Ot) * (1.0 - Om);                                           //  combined opacity
         pix3[3] = 255.0 * Ob;
      }
   }

   rad = attr.emboss[0];                                                               //  emboss text pixels in image
   depth = attr.emboss[1];

   if (rad && depth)
   {
      E9pxm = PXM_copy_area(E3pxm,orgx2,orgy2,ww2,hh2);

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

      for (dy = -rad; dy <= rad; dy++)                                                 //  build kernel with radius and depth
      for (dx = -rad; dx <= rad; dx++)
         kernel[dx+rad][dy+rad] = R * (dx + dy);
      kernel[rad][rad] = 1;                                                            //  kernel center cell = 1

      for (py1 = 0; py1 < hh2; py1++)                                                  //  loop all pixels in text image
      for (px1 = 0; px1 < ww2; px1++)
      {
         pixT = PXBpix(attr.pxb_text,px1,py1);                                         //  text pixel
         e3part = pixT[3] / 256.0;                                                     //  transparency
         if (e3part > 0.99) continue;                                                  //  omit transparent areas

         px3 = orgx2 + px1;                                                            //  image3 pixel
         py3 = orgy2 + py1;

         if (px3 < 0 || px3 >= Eww) continue;                                          //  omit parts beyond edges
         if (py3 < 0 || py3 >= Ehh) continue;

         pix3 = PXMpix(E3pxm,px3,py3);                                                 //  text in image pixel

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

            for (dy = -rad; dy <= rad; dy++)                                           //  loop surrounding block of pixels
            for (dx = -rad; dx <= rad; dx++)
            {
               if (px1+dx < 0 || px1+dx >= ww2) continue;
               if (py1+dy < 0 || py1+dy >= hh2) continue;
               pixN = PXMpix(E9pxm,px1+dx,py1+dy);
               R = kernel[dx+rad][dy+rad];
               sumpix += R * pixN[rgb];
            }

            if (sumpix < 0) sumpix = 0;
            if (sumpix > 255) sumpix = 255;
            pix3[rgb] = sumpix;
         }
      }

      PXM_free(E9pxm);
   }

   if (textpresent) {
      Fpaint3(orgx1,orgy1,ww1,hh1,cr);                                                 //  update window to erase old text
      textpresent = 0;
      CEF->Fmods--;
   }
                                                                                       //  (updates together to reduce flicker)
   Fpaint3(orgx2,orgy2,ww2,hh2,cr);                                                    //  update window for new text

   draw_context_destroy(draw_context);

   textpresent = 1;                                                                    //  mark text is present
   CEF->Fmods++;                                                                       //  increase mod count
   CEF->Fsaved = 0;

   orgx1 = orgx2;                                                                      //  remember overlap rectangle
   orgy1 = orgy2;                                                                      //    for next call
   ww1 = ww2;
   hh1 = hh2;
   return;
}


//  load text and attributes into dialog from a file

void load_text(zdialog *zd)
{
   FILE        *fid;
   int         err, nn;
   ch          *pp, *pp2, *file, buff[1200];
   ch          *dialogtitle = TX("load text data from a file");
   textattr_t  attr;

   file = zgetfile(dialogtitle,MWIN,"file",markup_folder);                             //  get input file from user
   if (! file) return;

   fid = fopen(file,"r");                                                              //  open for read
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      zfree(file);
      return;
   }

   pp = fgets_trim(buff,1200,fid);                                                     //  read text string
   if (! pp) goto badfile;
   if (! strmatchN(pp,"text string: ",13)) goto badfile;
   pp += 13;
   if (strlen(pp) < 2) goto badfile;
   repl_Nstrs(pp,attr.text,999,"\\n","\n",null);                                       //  replace "\n" with newline char.

   pp = fgets_trim(buff,1200,fid);                                                     //  read font and size
   if (! pp) goto badfile;
   if (! strmatchN(pp,"font: ",6)) goto badfile;
   pp += 6;
   strTrim(pp);
   pp2 = (ch *) zstrstr(pp,"size: ");
   if (! pp2) goto badfile;
   *pp2 = 0;
   pp2 += 6;
   strncpy0(attr.font,pp,80);
   attr.size = atoi(pp2);
   if (attr.size < 8) goto badfile;

   pp = fgets_trim(buff,1200,fid);                                                     //  read text attributes
   if (! pp) goto badfile;
   nn = sscanf(pp,"attributes: %f  %s %s %s %s  %d %d %d %d  %d %d %d",
            &attr.angle, attr.color[0], attr.color[1], attr.color[2], attr.color[3],
            &attr.transp[0], &attr.transp[1], &attr.transp[2], &attr.transp[3],
            &attr.towidth, &attr.shwidth, &attr.shangle);
   if (nn != 12) goto badfile;

   err = fclose(fid);
   if (err) {
      zmessageACK(Mwin,strerror(errno));
      zfree(file);
      return;
   }

   zdialog_stuff(zd,"text",attr.text);                                                 //  stuff zdialog fields
   zdialog_stuff(zd,"fontname",attr.font);
   zdialog_stuff(zd,"fontsize",attr.size);
   zdialog_stuff(zd,"txangle",attr.angle);
   zdialog_stuff(zd,"txcolor",attr.color[0]);
   zdialog_stuff(zd,"bgcolor",attr.color[1]);
   zdialog_stuff(zd,"tocolor",attr.color[2]);
   zdialog_stuff(zd,"shcolor",attr.color[3]);
   zdialog_stuff(zd,"txtransp",attr.transp[0]);
   zdialog_stuff(zd,"bgtransp",attr.transp[1]);
   zdialog_stuff(zd,"totransp",attr.transp[2]);
   zdialog_stuff(zd,"shtransp",attr.transp[3]);
   zdialog_stuff(zd,"towidth",attr.towidth);
   zdialog_stuff(zd,"shwidth",attr.shwidth);
   zdialog_stuff(zd,"shangle",attr.shangle);
   return;

badfile:                                                                               //  project file had a problem
   fclose(fid);
   zmessageACK(Mwin,TX("text file is defective"));
   printf("*** text file: %s\n",buff);
}


//  save text and attributes from dialog to a file

void save_text(zdialog *zd)
{
   ch          *dialogtitle = TX("save text data to a file");
   FILE        *fid;
   ch          *file, text2[1200];
   textattr_t  attr;

   file = zgetfile(dialogtitle,MWIN,"save",markup_folder);                             //  get output file from user
   if (! file) return;

   fid = fopen(file,"w");                                                              //  open for write
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      zfree(file);
      return;
   }

   zdialog_fetch(zd,"text",attr.text,1000);                                            //  get text and attributes from zdialog
   zdialog_fetch(zd,"fontname",attr.font,80);
   zdialog_fetch(zd,"fontsize",attr.size);
   zdialog_fetch(zd,"txangle",attr.angle);
   zdialog_fetch(zd,"txcolor",attr.color[0],20);
   zdialog_fetch(zd,"bgcolor",attr.color[1],20);
   zdialog_fetch(zd,"tocolor",attr.color[2],20);
   zdialog_fetch(zd,"shcolor",attr.color[3],20);
   zdialog_fetch(zd,"txtransp",attr.transp[0]);
   zdialog_fetch(zd,"bgtransp",attr.transp[1]);
   zdialog_fetch(zd,"totransp",attr.transp[2]);
   zdialog_fetch(zd,"shtransp",attr.transp[3]);
   zdialog_fetch(zd,"towidth",attr.towidth);
   zdialog_fetch(zd,"shwidth",attr.shwidth);
   zdialog_fetch(zd,"shangle",attr.shangle);

   repl_Nstrs(attr.text,text2,1200,"\n","\\n",null);                                   //  replace newlines with "\n"

   fprintf(fid,"text string: %s\n",text2);

   fprintf(fid,"font: %s  size: %d \n",attr.font, attr.size);

   fprintf(fid,"attributes: %.4f  %s %s %s %s  %d %d %d %d  %d %d %d \n",
            attr.angle, attr.color[0], attr.color[1], attr.color[2], attr.color[3],
            attr.transp[0], attr.transp[1], attr.transp[2], attr.transp[3],
            attr.towidth, attr.shwidth, attr.shangle);

   fclose(fid);
   return;
}


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

   Create a graphic image with text, any color, any font, any angle.
   Add outline and shadow colors. Apply transparencies.

   struct textattr_t                                           //  attributes for gentext() function
      ch       text[1000];                                     //  text to generate image from
      ch       font[80];                                       //  font name
      int      size;                                           //  font size
      int      tww, thh;                                       //  generated image size, unrotated
      float    angle;                                          //  text angle, degrees
      float    sinT, cosT;                                     //  trig funcs for text angle
      ch       color[4][20];                                   //  text, backing, outline, shadow "R|G|B"
      int      transp[4];                                      //  corresponding transparencies 0-255
      int      towidth;                                        //  outline width, pixels
      int      shwidth;                                        //  shadow width
      int      shangle;                                        //  shadow angle -180...+180
      PXB      *pxb_text;                                      //  output image with text/outline/shadow

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

int gentext(textattr_t *attr)
{
   PXB * gentext_outline(textattr_t *, PXB *);
   PXB * gentext_shadow(textattr_t *, PXB *);

   PangoFontDescription    *pfont;
   PangoLayout             *playout;
   cairo_surface_t         *surface;
   cairo_t                 *cr;

   float          angle;
   int            txred, txgreen, txblue;
   int            bgred, bggreen, bgblue;
   int            tored, togreen, toblue;
   int            shred, shgreen, shblue;
   float          txtransp, bgtransp, totransp, shtransp;

   PXB            *pxb_temp1, *pxb_temp2;
   uint8          *cairo_data, *cpix;
   uint8          *pix1, *pix2;
   int            px, py, ww, hh;
   ch             font[100];                                                           //  font name and size
   int            red, green, blue;
   float          txpart, topart, shpart, bgpart;

   if (! *attr->text) strcpy(attr->text," ");                                          //  no text

   if (attr->pxb_text) PXB_free(attr->pxb_text);

   snprintf(font,100,"%s %d",attr->font,attr->size);                                   //  font name and size together
   angle = attr->angle;                                                                //  text angle, degrees
   attr->sinT = sin(angle/57.296);                                                     //  trig funcs for text angle
   attr->cosT = cos(angle/57.296);
   txred = atoi(substring(attr->color[0],'|',1));                                      //  get text foreground color
   txgreen = atoi(substring(attr->color[0],'|',2));
   txblue = atoi(substring(attr->color[0],'|',3));
   bgred = atoi(substring(attr->color[1],'|',1));                                      //  get text background color
   bggreen = atoi(substring(attr->color[1],'|',2));
   bgblue = atoi(substring(attr->color[1],'|',3));
   tored = atoi(substring(attr->color[2],'|',1));                                      //  get text outline color
   togreen = atoi(substring(attr->color[2],'|',2));
   toblue = atoi(substring(attr->color[2],'|',3));
   shred = atoi(substring(attr->color[3],'|',1));                                      //  get text shadow color
   shgreen = atoi(substring(attr->color[3],'|',2));
   shblue = atoi(substring(attr->color[3],'|',3));
   txtransp = 0.01 * attr->transp[0];                                                  //  get transparencies
   bgtransp = 0.01 * attr->transp[1];                                                  //  text, background, outline, shadow
   totransp = 0.01 * attr->transp[2];
   shtransp = 0.01 * attr->transp[3];

   pfont = pango_font_description_from_string(font);                                   //  make layout with text
   playout = gtk_widget_create_pango_layout(Fdrawin,null);                             //  Fdrawin instead of Cdrawin
   if (! playout) zappcrash("gentext(): cannot create pango layout");
   pango_layout_set_font_description(playout,pfont);
   pango_layout_set_text(playout,attr->text,-1);

   pango_layout_get_pixel_size(playout,&ww,&hh);
   ww += 2 + 0.2 * attr->size;                                                         //  compensate bad font metrics
   hh += 2 + 0.1 * attr->size;
   attr->tww = ww;                                                                     //  save text image size before rotate
   attr->thh = hh;

   surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24,ww,hh);                     //  cairo output image
   cr = cairo_create(surface);
   pango_cairo_show_layout(cr,playout);                                                //  write text layout to image

   cairo_data = cairo_image_surface_get_data(surface);                                 //  get text image pixels

   pxb_temp1 = PXB_make(ww+5,hh,3);                                                    //  create PXB

   for (py = 0; py < hh; py++)                                                         //  copy text image to PXB
   for (px = 0; px < ww; px++)
   {
      cpix = cairo_data + 4 * (ww * py + px);                                          //  pango output is monocolor
      pix2 = PXBpix(pxb_temp1,px+4,py);                                                //  small pad before text
      pix2[0] = cpix[3];                                                               //  use red [0] for text intensity
      pix2[1] = pix2[2] = 0;
   }

   pango_font_description_free(pfont);                                                 //  free resources
   g_object_unref(playout);
   cairo_destroy(cr);
   cairo_surface_destroy(surface);

   pxb_temp2 = gentext_outline(attr,pxb_temp1);                                        //  add text outline if any
   if (pxb_temp2) {                                                                    //    using green [1] for outline intensity
      PXB_free(pxb_temp1);
      pxb_temp1 = pxb_temp2;
   }

   pxb_temp2 = gentext_shadow(attr,pxb_temp1);                                         //  add text shadow color if any
   if (pxb_temp2) {                                                                    //    using blue [2] for shadow intensity
      PXB_free(pxb_temp1);
      pxb_temp1 = pxb_temp2;
   }

   if (fabsf(angle) > 0.1) {                                                           //  rotate text if wanted
      pxb_temp2 = PXB_rotate(pxb_temp1,angle);
      PXB_free(pxb_temp1);
      pxb_temp1 = pxb_temp2;
   }

   ww = pxb_temp1->ww;                                                                 //  text image input PXB
   hh = pxb_temp1->hh;
   pxb_temp2 = PXB_make(ww,hh,4);                                                      //  text image output PXB

   for (py = 0; py < hh; py++)                                                         //  loop all pixels in text image
   for (px = 0; px < ww; px++)
   {
      pix1 = PXBpix(pxb_temp1,px,py);                                                  //  copy-from pixel (text + outline + shadow)
      pix2 = PXBpix(pxb_temp2,px,py);                                                  //  copy-to pixel

      txpart = pix1[0] / 256.0;                                                        //  text opacity 0-1
      topart = pix1[1] / 256.0;                                                        //  outline
      shpart = pix1[2] / 256.0;                                                        //  shadow
      bgpart = (1.0 - txpart - topart - shpart);                                       //  background
      txpart = txpart * (1.0 - txtransp);                                              //  reduce for transparencies
      topart = topart * (1.0 - totransp);
      shpart = shpart * (1.0 - shtransp);
      bgpart = bgpart * (1.0 - bgtransp);

      red = txpart * txred + topart * tored + shpart * shred + bgpart * bgred;
      green = txpart * txgreen + topart * togreen + shpart * shgreen + bgpart * bggreen;
      blue = txpart * txblue + topart * toblue + shpart * shblue + bgpart * bgblue;

      pix2[0] = red;                                                                   //  output total red, green, blue
      pix2[1] = green;
      pix2[2] = blue;

      pix2[3] = 255 * (1.0 - txpart - topart - shpart - bgpart);                       //  image part visible through text
   }

   PXB_free(pxb_temp1);
   attr->pxb_text = pxb_temp2;
   return 0;
}


//  add an outline color to the text character edges
//  red color [0] is original monocolor text
//  use green color [1] for added outline

PXB * gentext_outline(textattr_t *attr, PXB *pxb1)
{
   PXB         *pxb2;
   int         toww, ww1, hh1, ww2, hh2;
   int         px, py, dx, dy;
   uint8       *pix1, *pix2;
   float       theta;

   toww = attr->towidth;                                                               //  text outline color width
   if (toww == 0) return 0;                                                            //  zero

   ww1 = pxb1->ww;                                                                     //  input PXB dimensions
   hh1 = pxb1->hh;
   ww2 = ww1 + toww * 2;                                                               //  add margins for outline width
   hh2 = hh1 + toww * 2;
   pxb2 = PXB_make(ww2,hh2,3);                                                         //  output PXB

   for (py = 0; py < hh1; py++)                                                        //  copy text pixels to outline pixels
   for (px = 0; px < ww1; px++)                                                        //    displaced by outline width
   {
      pix1 = PXBpix(pxb1,px,py);
      pix2 = PXBpix(pxb2,px+toww,py+toww);
      pix2[0] = pix1[0];
   }

   theta = 0.7 / toww;
   for (theta = 0; theta < 6.3; theta += 0.7/toww)                                     //  displace outline pixels in all directions
   {
      dx = roundf(toww * sinf(theta));
      dy = roundf(toww * cosf(theta));

      for (py = 0; py < hh1; py++)
      for (px = 0; px < ww1; px++)
      {
         pix1 = PXBpix(pxb1,px,py);
         pix2 = PXBpix(pxb2,px+toww+dx,py+toww+dy);
         if (pix2[1] < pix1[0] - pix2[0])                                              //  compare text to outline brightness
            pix2[1] = pix1[0] - pix2[0];                                               //  brighter part is outline pixel
      }
   }

   return pxb2;                                                                        //  pix2[0] / pix2[1] = text / outline
}


//  add a shadow to the text character edges
//  red color [0] is original monocolor text intensity
//  green color [1] is added outline if any
//  use blue color [2] for added shadow

PXB * gentext_shadow(textattr_t *attr, PXB *pxb1)
{
   PXB         *pxb2;
   int         shww, ww1, hh1, ww2, hh2;
   int         px, py, dx, dy;
   uint8       *pix1, *pix2;
   float       theta;

   shww = attr->shwidth;                                                               //  text shadow width
   if (shww == 0) return 0;                                                            //  zero

   ww1 = pxb1->ww;                                                                     //  input PXB dimensions
   hh1 = pxb1->hh;
   ww2 = ww1 + shww * 2;                                                               //  add margins for shadow width
   hh2 = hh1 + shww * 2;
   pxb2 = PXB_make(ww2,hh2,3);                                                         //  output PXB

   for (py = 0; py < hh1; py++)                                                        //  copy text pixels to shadow pixels
   for (px = 0; px < ww1; px++)                                                        //    displaced by shadow width
   {
      pix1 = PXBpix(pxb1,px,py);
      pix2 = PXBpix(pxb2,px+shww,py+shww);
      pix2[0] = pix1[0];
      pix2[1] = pix1[1];
   }

   theta = (90 - attr->shangle) / 57.3;                                                //  degrees to radians, 0 = to the right
   dx = roundf(shww * sinf(theta));
   dy = roundf(shww * cosf(theta));

   for (py = 0; py < hh1; py++)                                                        //  displace text by shadow width
   for (px = 0; px < ww1; px++)
   {
      pix1 = PXBpix(pxb1,px,py);
      pix2 = PXBpix(pxb2,px+shww+dx,py+shww+dy);
      if (pix2[2] < pix1[0] + pix1[1] - pix2[0] - pix2[1])                             //  compare text+outline to shadow pixels
         pix2[2] = pix1[0] + pix1[1] - pix2[0] - pix2[1];                              //  brighter part is shadow pixel
   }

   return pxb2;                                                                        //  pix2[0] / pix2[1] / pix2[2]
}                                                                                      //    = text / outline / shadow brightness


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

//  draw a line or arrow on top of the image

namespace drawline_names
{
   lineattr_t  attr;                                                                   //  line/arrow attributes and image

   int      mpx, mpy;                                                                  //  mouse position on image
   int      linepresent;                                                               //  flag, line present on image
   int      orgx1, orgy1, ww1, hh1;                                                    //  old line image overlap rectangle
   int      orgx2, orgy2, ww2, hh2;                                                    //  new overlap rectangle
   zdialog  *zd;

   int   dialog_event(zdialog *zd, ch *event);                                         //  dialog event function
   void  mousefunc();                                                                  //  mouse event function
   void  write(int mode);                                                              //  write line on image

   editfunc    EFdrawline;
}


void m_draw_line(GtkWidget *, ch *menu)
{
   using namespace drawline_names;

   ch       *intro = TX("Enter line or arrow properties in dialog, \n"
                        "click/drag on image, right click to remove");

   printf("m_draw_line \n");

   EFdrawline.menufunc = m_draw_line;
   EFdrawline.menuname = "Markup Line";
   EFdrawline.Farea = 1;                                                               //  select area ignored
   EFdrawline.mousefunc = mousefunc;

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

/***
       ________________________________________________
      |          Draw line or arrow on image           |
      |                                                |
      |  Enter line or arrow properties in dialog,     |
      |  click/drag on image, right click to remove.   |
      |                                                |
      |  Line length [____]  width [____]              |
      |  Arrow head  [x] left   [x] right              |
      |                                                |
      |            Color  Transp. Width  Angle         |
      |  line      [###]  [____]         [___]         |                         lncolor lntransp lnangle
      |  backing   [###]  [____]                       |                         bgcolor bgtransp
      |  outline   [###]  [____]  [___]                |                         tocolor totransp towidth
      |  shadow    [###]  [____]  [___]  [___]         |                         shcolor shtransp shwidth shangle
      |                                                |
      |                              [Commit] [OK] [X] |
      |________________________________________________|

***/

   zd = zdialog_new(TX("Draw line or arrow on image"),Mwin,"Commit","OK","X",null);
   EFdrawline.zd = zd;
   EFdrawline.mousefunc = mousefunc;
   EFdrawline.menufunc = m_draw_line;                                                  //  allow restart

   zdialog_add_widget(zd,"label","intro","dialog",intro,"space=3");

   zdialog_add_widget(zd,"hbox","hbline","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lablength","hbline",TX("Line length"),"space=5");
   zdialog_add_widget(zd,"zspin","length","hbline","2|9999|1|20");
   zdialog_add_widget(zd,"label","space","hbline",0,"space=10");
   zdialog_add_widget(zd,"label","labwidth","hbline",TX("Width"),"space=5");
   zdialog_add_widget(zd,"zspin","width","hbline","1|99|1|2");

   zdialog_add_widget(zd,"hbox","hbarrow","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labarrow","hbarrow",TX("Arrow head"),"space=5");
   zdialog_add_widget(zd,"check","larrow","hbarrow",TX("Left"));
   zdialog_add_widget(zd,"label","space","hbarrow",0,"space=10");
   zdialog_add_widget(zd,"check","rarrow","hbarrow",TX("Right"));

   zdialog_add_widget(zd,"hbox","hbcol","dialog");
   zdialog_add_widget(zd,"vbox","vbcol1","hbcol",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbcol2","hbcol",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbcol3","hbcol",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbcol4","hbcol",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbcol5","hbcol",0,"homog|space=5");

   zdialog_add_widget(zd,"label","space","vbcol1");
   zdialog_add_widget(zd,"label","labline","vbcol1",TX("line"));
   zdialog_add_widget(zd,"label","labback","vbcol1",TX("backing"));
   zdialog_add_widget(zd,"label","laboutln","vbcol1",TX("outline"));
   zdialog_add_widget(zd,"label","labshadow","vbcol1",TX("shadow"));

   zdialog_add_widget(zd,"label","labcol","vbcol2",TX("Color"));
   zdialog_add_widget(zd,"colorbutt","lncolor","vbcol2","0|0|0");
   zdialog_add_widget(zd,"colorbutt","bgcolor","vbcol2","255|255|255");
   zdialog_add_widget(zd,"colorbutt","tocolor","vbcol2","255|0|0");
   zdialog_add_widget(zd,"colorbutt","shcolor","vbcol2","255|0|0");

   zdialog_add_widget(zd,"label","labcol","vbcol3",TX("Transp."));
   zdialog_add_widget(zd,"zspin","lntransp","vbcol3","0|100|1|0");
   zdialog_add_widget(zd,"zspin","bgtransp","vbcol3","0|100|1|0");
   zdialog_add_widget(zd,"zspin","totransp","vbcol3","0|100|1|0");
   zdialog_add_widget(zd,"zspin","shtransp","vbcol3","0|100|1|0");

   zdialog_add_widget(zd,"label","labw","vbcol4",TX("Width"));
   zdialog_add_widget(zd,"label","space","vbcol4");
   zdialog_add_widget(zd,"label","space","vbcol4");
   zdialog_add_widget(zd,"zspin","towidth","vbcol4","0|30|1|0");
   zdialog_add_widget(zd,"zspin","shwidth","vbcol4","0|50|1|0");

   zdialog_add_widget(zd,"label","labw","vbcol5",TX("Angle"));
   zdialog_add_widget(zd,"zspin","lnangle","vbcol5","-360|360|0.1|0");
   zdialog_add_widget(zd,"label","space","vbcol5");
   zdialog_add_widget(zd,"label","space","vbcol5");
   zdialog_add_widget(zd,"zspin","shangle","vbcol5","-360|360|1|0");

   zdialog_add_ttip(zd,"Commit",TX("fix line in layout and start new line"));

   zdialog_load_inputs(zd);                                                            //  restore prior inputs

   memset(&attr,0,sizeof(attr));

   zdialog_fetch(zd,"length",attr.length);                                             //  get defaults or prior inputs
   zdialog_fetch(zd,"width",attr.width);
   zdialog_fetch(zd,"larrow",attr.larrow);
   zdialog_fetch(zd,"rarrow",attr.rarrow);
   zdialog_fetch(zd,"lnangle",attr.angle);
   zdialog_fetch(zd,"lncolor",attr.color[0],20);
   zdialog_fetch(zd,"bgcolor",attr.color[1],20);
   zdialog_fetch(zd,"tocolor",attr.color[2],20);
   zdialog_fetch(zd,"shcolor",attr.color[3],20);
   zdialog_fetch(zd,"lntransp",attr.transp[0]);
   zdialog_fetch(zd,"bgtransp",attr.transp[1]);
   zdialog_fetch(zd,"totransp",attr.transp[2]);
   zdialog_fetch(zd,"shtransp",attr.transp[3]);
   zdialog_fetch(zd,"towidth",attr.towidth);
   zdialog_fetch(zd,"shwidth",attr.shwidth);
   zdialog_fetch(zd,"shangle",attr.shangle);

   genline(&attr);                                                                     //  generate initial line

   takeMouse(mousefunc,dragcursor);                                                    //  connect mouse function
   linepresent = 0;                                                                    //  no line on image yet
   mpx = mpy = -1;                                                                     //  no position defined yet

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


//  dialog event and completion callback function

int drawline_names::dialog_event(zdialog *zd, ch *event)
{
   using namespace drawline_names;
   
   int      yn;

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                            //  [Commit] current line to image
         zd->zstat = 0;
         write(1);
         linepresent = 0;                                                              //  (no old line to erase)
         mpx = mpy = -1;
         PXM_free(E1pxm);                                                              //  copy edits in E3 to source image E1
         E1pxm = PXM_copy(E3pxm);
         Fpaintnow();                                                                  //  update image synchronous
         return 1;
      }

      if (zd->zstat == 2) {
         if (CEF->Fmods) edit_done(0);                                                 //  [OK], complete pending edit
         else edit_cancel(0);
         if (attr.pxb_line) PXB_free(attr.pxb_line);
         m_markup(0,0);                                                                //  restore main markup dialog
         return 1;
      }
      
      if (CEF->Fmods) {
         yn = zmessageYN(Mwin,TX("discard changes?"));                                 //  unsaved changes
         if (! yn) {
            zd->zstat = 0;                                                             //  back out
            return 1;
         }
      }

      edit_cancel(0);                                                                  //  cancel, no changes
      if (attr.pxb_line) PXB_free(attr.pxb_line);
      m_markup(0,0);                                                                   //  restore main markup dialog
      return 1;
   }

   if (strmatch(event,"focus")) {                                                      //  toggle mouse capture
      takeMouse(mousefunc,dragcursor);                                                 //  connect mouse function
      return 1;
   }

   if (strmatch(event,"length"))                                                       //  line length
      zdialog_fetch(zd,"length",attr.length);

   if (strmatch(event,"width"))                                                        //  line width
      zdialog_fetch(zd,"width",attr.width);

   if (strmatch(event,"larrow"))                                                       //  left arrow head
      zdialog_fetch(zd,"larrow",attr.larrow);

   if (strmatch(event,"rarrow"))                                                       //  right arrow head
      zdialog_fetch(zd,"rarrow",attr.rarrow);

   if (strmatch(event,"lnangle"))                                                      //  line angle
      zdialog_fetch(zd,"lnangle",attr.angle);

   if (strmatch(event,"lncolor"))                                                      //  foreground (line) color
      zdialog_fetch(zd,"lncolor",attr.color[0],20);

   if (strmatch(event,"bgcolor"))                                                      //  background color
      zdialog_fetch(zd,"bgcolor",attr.color[1],20);

   if (strmatch(event,"tocolor"))                                                      //  line outline color
      zdialog_fetch(zd,"tocolor",attr.color[2],20);

   if (strmatch(event,"shcolor"))                                                      //  line shadow color
      zdialog_fetch(zd,"shcolor",attr.color[3],20);

   if (strmatch(event,"lntransp"))                                                     //  foreground transparency
      zdialog_fetch(zd,"lntransp",attr.transp[0]);

   if (strmatch(event,"bgtransp"))                                                     //  background transparency
      zdialog_fetch(zd,"bgtransp",attr.transp[1]);

   if (strmatch(event,"totransp"))                                                     //  line outline transparency
      zdialog_fetch(zd,"totransp",attr.transp[2]);

   if (strmatch(event,"shtransp"))                                                     //  line shadow transparency
      zdialog_fetch(zd,"shtransp",attr.transp[3]);

   if (strmatch(event,"towidth"))                                                      //  line outline width
      zdialog_fetch(zd,"towidth",attr.towidth);

   if (strmatch(event,"shwidth"))                                                      //  line shadow width
      zdialog_fetch(zd,"shwidth",attr.shwidth);

   if (strmatch(event,"shangle"))                                                      //  line shadow angle
      zdialog_fetch(zd,"shangle",attr.shangle);

   genline(&attr);                                                                     //  build line image from attributes
   write(1);                                                                           //  write on image
   return 1;
}


//  mouse function, set new position for line on image

void drawline_names::mousefunc()
{
   using namespace drawline_names;

   float    ax1, ay1, ax2, ay2, ax3, ay3;                                              //  line/arrow end points, mid point
   float    angle, rad, l2;
   float    amx, amy;
   float    d1, d2, d3, sinv;

   if (RMclick) {                                                                      //  right mouse click
      write(2);                                                                        //  erase old line, if any
      mpx = mpy = -1;
      LMclick = RMclick = Mxdrag = Mydrag = 0;
      return;
   }

   if (LMclick + Mxdrag + Mydrag == 0) return;

   if (LMclick) {
      mpx = Mxclick;                                                                   //  new line position on image
      mpy = Myclick;
   }
   else {
      mpx = Mxdrag;
      mpy = Mydrag;
   }

   LMclick = RMclick = Mxdrag = Mydrag = 0;

   if (! linepresent) {
      orgx2 = mpx;
      orgy2 = mpy;
      write(1);
      return;
   }

   angle = attr.angle;                                                                 //  move closest line endpoint to mouse
   rad = -angle / 57.296;                                                              //    posn and leave other endpoint fixed
   l2 = attr.length / 2.0;

   ww2 = attr.pxb_line->ww;                                                            //  line image buffer
   hh2 = attr.pxb_line->hh;

   amx = ww2 / 2.0;                                                                    //  line midpoint within line image
   amy = hh2 / 2.0;

   ax1 = amx - l2 * cosf(rad) + 0.5;                                                   //  line end points
   ay1 = amy + l2 * sinf(rad) + 0.5;
   ax2 = amx + l2 * cosf(rad) + 0.5;
   ay2 = amy - l2 * sinf(rad) + 0.5;
   ax3 = (ax1 + ax2) / 2;                                                              //  line mid point
   ay3 = (ay1 + ay2) / 2;

   d1 = (mpx-ax1-orgx1) * (mpx-ax1-orgx1) + (mpy-ay1-orgy1) * (mpy-ay1-orgy1);
   d2 = (mpx-ax2-orgx1) * (mpx-ax2-orgx1) + (mpy-ay2-orgy1) * (mpy-ay2-orgy1);
   d3 = (mpx-amx-orgx1) * (mpx-amx-orgx1) + (mpy-amy-orgy1) * (mpy-amy-orgy1);

   d1 = sqrtf(d1);                                                                     //  mouse - end point distances
   d2 = sqrtf(d2);
   d3 = sqrtf(d3);                                                                     //  mouse - mid point distance

   if (d1 < d2 && d1 < d3) {                                                           //  move ax1/ay1 end to mouse
      ax2 += orgx1;
      ay2 += orgy1;
      ax1 = mpx;
      ay1 = mpy;
      attr.length = d2 + 0.5;
      sinv = (ay1-ay2) / d2;
      if (sinv > 1.0) sinv = 1.0;
      if (sinv < -1.0) sinv = -1.0;
      rad = asinf(sinv);
      angle = -57.296 * rad;
      if (mpx > ax2) angle = -180 - angle;
   }

   else if (d2 < d1 && d2 < d3) {                                                      //  move ax2/ay2 end to mouse
      ax1 += orgx1;
      ay1 += orgy1;
      ax2 = mpx;
      ay2 = mpy;
      attr.length = d1 + 0.5;
      sinv = (ay1-ay2) / d1;
      if (sinv > 1.0) sinv = 1.0;
      if (sinv < -1.0) sinv = -1.0;
      rad = asinf(sinv);
      angle = -57.296 * rad;
      if (mpx < ax1) angle = -180 - angle;
   }

   else {                                                                              //  move entire arrow unchanged
      ax1 += (mpx - ax3);
      ay1 += (mpy - ay3);
      ax2 += (mpx - ax3);
      ay2 += (mpy - ay3);
   }

   if (angle < -180) angle += 360;
   if (angle > 180) angle -= 360;
   attr.angle = angle;
   genline(&attr);
   ww2 = attr.pxb_line->ww;
   hh2 = attr.pxb_line->hh;
   amx = (ax1 + ax2) / 2.0;
   amy = (ay1 + ay2) / 2.0;
   orgx2 = amx - ww2 / 2.0;
   orgy2 = amy - hh2 / 2.0;
   write(1);

   zdialog_stuff(zd,"lnangle",attr.angle);
   zdialog_stuff(zd,"length",attr.length);
   return;
}


//  write line on image at designated location
//  mode: 1  erase old and write to new position
//        2  erase old and write nothing

void drawline_names::write(int mode)
{
   using namespace drawline_names;

   float       *pix1, *pix3;
   uint8       *pixL;
   int         px1, py1, px3, py3, done;
   float       e3part, Ot, Om, Ob;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   if (linepresent)
   {
      for (py3 = orgy1; py3 < orgy1 + hh1; py3++)                                      //  erase prior line image
      for (px3 = orgx1; px3 < orgx1 + ww1; px3++)                                      //  replace E3 pixels with E1 pixels
      {                                                                                //    in prior overlap rectangle
         if (px3 < 0 || px3 >= Eww) continue;
         if (py3 < 0 || py3 >= Ehh) continue;
         pix1 = PXMpix(E1pxm,px3,py3);
         pix3 = PXMpix(E3pxm,px3,py3);
         memcpy(pix3,pix1,pcc);
      }
   }

   done = 0;
   if (mode == 2) done = 1;                                                            //  erase only
   if (mpx < 0 && mpy < 0) done = 1;                                                   //  no position defined

   if (done) {
      if (linepresent) {
         Fpaint3(orgx1,orgy1,ww1,hh1,cr);                                              //  update window to erase old line
         linepresent = 0;                                                              //  mark no line present
      }

      draw_context_destroy(draw_context);
      return;
   }

   ww2 = attr.pxb_line->ww;                                                            //  line image buffer
   hh2 = attr.pxb_line->hh;

   for (py1 = 0; py1 < hh2; py1++)                                                     //  loop all pixels in line image
   for (px1 = 0; px1 < ww2; px1++)
   {
      px3 = orgx2 + px1;                                                               //  copy-to image3 pixel
      py3 = orgy2 + py1;

      if (px3 < 0 || px3 >= Eww) continue;                                             //  omit parts beyond edges
      if (py3 < 0 || py3 >= Ehh) continue;

      pixL = PXBpix(attr.pxb_line,px1,py1);                                            //  copy-from line pixel
      pix3 = PXMpix(E3pxm,px3,py3);                                                    //  copy-to image pixel

      e3part = pixL[3] / 256.0;                                                        //  line image transparency

      pix3[0] = pixL[0] + e3part * pix3[0];                                            //  combine line part + image part
      pix3[1] = pixL[1] + e3part * pix3[1];
      pix3[2] = pixL[2] + e3part * pix3[2];

      if (nc > 3) {
         Ot = (1.0 - e3part);                                                          //  line opacity
         Om = pix3[3] / 256.0;                                                         //  image opacity
         Ob = 1.0 - (1.0 - Ot) * (1.0 - Om);                                           //  combined opacity
         pix3[3] = 255.0 * Ob;
      }
   }

   if (linepresent) {
      Fpaint3(orgx1,orgy1,ww1,hh1,cr);                                                 //  update window to erase old line
      linepresent = 0;
   }
                                                                                       //  (updates together to reduce flicker)
   Fpaint3(orgx2,orgy2,ww2,hh2,cr);                                                    //  update window for new line

   draw_context_destroy(draw_context);

   CEF->Fmods++;
   CEF->Fsaved = 0;
   linepresent = 1;                                                                    //  mark line is present

   orgx1 = orgx2;                                                                      //  remember overlap rectangle
   orgy1 = orgy2;                                                                      //    for next call
   ww1 = ww2;
   hh1 = hh2;

   return;
}


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

//  Create a graphic image of a line or arrow, any color,
//  any size, any angle. Add outline and shadow colors.

int genline(lineattr_t *attr)
{
   PXB * genline_outline(lineattr_t *, PXB *);
   PXB * genline_shadow(lineattr_t *, PXB *);

   cairo_surface_t         *surface;
   cairo_t                 *cr;

   float          angle;
   int            lnred, lngreen, lnblue;
   int            bgred, bggreen, bgblue;
   int            tored, togreen, toblue;
   int            shred, shgreen, shblue;
   float          lntransp, bgtransp, totransp, shtransp;

   PXB            *pxb_temp1, *pxb_temp2;
   uint8          *cairo_data, *cpix;
   uint8          *pix1, *pix2;
   float          length, width;
   int            px, py, ww, hh;
   int            red, green, blue;
   float          lnpart, topart, shpart, bgpart;

   if (attr->pxb_line) PXB_free(attr->pxb_line);

   angle = attr->angle;                                                                //  line angle, degrees
   attr->sinT = sin(angle/57.296);                                                     //  trig funcs for line angle
   attr->cosT = cos(angle/57.296);
   lnred = atoi(substring(attr->color[0],'|',1));                                      //  get line foreground color
   lngreen = atoi(substring(attr->color[0],'|',2));
   lnblue = atoi(substring(attr->color[0],'|',3));
   bgred = atoi(substring(attr->color[1],'|',1));                                      //  get line background color
   bggreen = atoi(substring(attr->color[1],'|',2));
   bgblue = atoi(substring(attr->color[1],'|',3));
   tored = atoi(substring(attr->color[2],'|',1));                                      //  get line outline color
   togreen = atoi(substring(attr->color[2],'|',2));
   toblue = atoi(substring(attr->color[2],'|',3));
   shred = atoi(substring(attr->color[3],'|',1));                                      //  get line shadow color
   shgreen = atoi(substring(attr->color[3],'|',2));
   shblue = atoi(substring(attr->color[3],'|',3));
   lntransp = 0.01 * attr->transp[0];                                                  //  get transparencies
   bgtransp = 0.01 * attr->transp[1];                                                  //  line, background, outline, shadow
   totransp = 0.01 * attr->transp[2];
   shtransp = 0.01 * attr->transp[3];

   length = attr->length;                                                              //  line dimensions
   width = attr->width;

   ww = length + 20;                                                                   //  create cairo surface
   hh = width + 20;                                                                    //    with margins all around

   if (attr->larrow || attr->rarrow)                                                   //  wider if arrow head used
      hh += width * 4;

   attr->lww = ww;
   attr->lhh = hh;

   surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24,ww,hh);
   cr = cairo_create(surface);

   cairo_set_antialias(cr,CAIRO_ANTIALIAS_BEST);
   cairo_set_line_width(cr,width);
   cairo_set_line_cap(cr,CAIRO_LINE_CAP_ROUND);

   cairo_move_to(cr, 10, hh/2.0);                                                      //  draw line in middle of surface
   cairo_line_to(cr, length+10, hh/2.0);

   if (attr->larrow) {                                                                 //  add arrow heads if req.
      cairo_move_to(cr, 10, hh/2.0);
      cairo_line_to(cr, 10 + 2 * width, hh/2.0 - 2 * width);
      cairo_move_to(cr, 10, hh/2.0);
      cairo_line_to(cr, 10 + 2 * width, hh/2.0 + 2 * width);
   }

   if (attr->rarrow) {
      cairo_move_to(cr, length+10, hh/2.0);
      cairo_line_to(cr, length+10 - 2 * width, hh/2.0 - 2 * width);
      cairo_move_to(cr, length+10, hh/2.0);
      cairo_line_to(cr, length+10 - 2 * width, hh/2.0 + 2 * width);
   }

   cairo_stroke(cr);

   cairo_data = cairo_image_surface_get_data(surface);                                 //  cairo image pixels

   pxb_temp1 = PXB_make(ww,hh,3);                                                      //  create PXB

   for (py = 0; py < hh; py++)                                                         //  copy image to PXB
   for (px = 0; px < ww; px++)
   {
      cpix = cairo_data + 4 * (ww * py + px);                                          //  pango output is monocolor
      pix2 = PXBpix(pxb_temp1,px,py);
      pix2[0] = cpix[3];                                                               //  use red [0] for line intensity
      pix2[1] = pix2[2] = 0;
   }

   cairo_destroy(cr);                                                                  //  free resources
   cairo_surface_destroy(surface);

   pxb_temp2 = genline_outline(attr,pxb_temp1);                                        //  add line outline if any
   if (pxb_temp2) {                                                                    //    using green [1] for outline intensity
      PXB_free(pxb_temp1);
      pxb_temp1 = pxb_temp2;
   }

   pxb_temp2 = genline_shadow(attr,pxb_temp1);                                         //  add line shadow color if any
   if (pxb_temp2) {                                                                    //    using blue [2] for shadow intensity
      PXB_free(pxb_temp1);
      pxb_temp1 = pxb_temp2;
   }

   if (fabsf(angle) > 0.1) {                                                           //  rotate line if wanted
      pxb_temp2 = PXB_rotate(pxb_temp1,angle);
      PXB_free(pxb_temp1);
      pxb_temp1 = pxb_temp2;
   }

   ww = pxb_temp1->ww;                                                                 //  line image input PXB
   hh = pxb_temp1->hh;
   pxb_temp2 = PXB_make(ww,hh,4);                                                      //  line image output PXB

   for (py = 0; py < hh; py++)                                                         //  loop all pixels in line image
   for (px = 0; px < ww; px++)
   {
      pix1 = PXBpix(pxb_temp1,px,py);                                                  //  copy-from pixel (line + outline + shadow)
      pix2 = PXBpix(pxb_temp2,px,py);                                                  //  copy-to pixel

      lnpart = pix1[0] / 256.0;
      topart = pix1[1] / 256.0;
      shpart = pix1[2] / 256.0;
      bgpart = (1.0 - lnpart - topart - shpart);
      lnpart = lnpart * (1.0 - lntransp);
      topart = topart * (1.0 - totransp);
      shpart = shpart * (1.0 - shtransp);
      bgpart = bgpart * (1.0 - bgtransp);

      red = lnpart * lnred + topart * tored + shpart * shred + bgpart * bgred;
      green = lnpart * lngreen + topart * togreen + shpart * shgreen + bgpart * bggreen;
      blue = lnpart * lnblue + topart * toblue + shpart * shblue + bgpart * bgblue;

      pix2[0] = red;                                                                   //  output total red, green blue
      pix2[1] = green;
      pix2[2] = blue;

      pix2[3] = 255 * (1.0 - lnpart - topart - shpart - bgpart);                       //  image part visible through line
   }

   PXB_free(pxb_temp1);
   attr->pxb_line = pxb_temp2;
   return 0;
}


//  add an outline color to the line edges
//  red color [0] is original monocolor line
//  use green color [1] for added outline

PXB * genline_outline(lineattr_t *attr, PXB *pxb1)
{
   PXB         *pxb2;
   int         toww, ww1, hh1, ww2, hh2;
   int         px, py, dx, dy;
   uint8       *pix1, *pix2;
   float       theta;

   toww = attr->towidth;                                                               //  line outline color width
   if (toww == 0) return 0;                                                            //  zero

   ww1 = pxb1->ww;                                                                     //  input PXB dimensions
   hh1 = pxb1->hh;
   ww2 = ww1 + toww * 2;                                                               //  add margins for outline width
   hh2 = hh1 + toww * 2;
   pxb2 = PXB_make(ww2,hh2,3);                                                         //  output PXB

   for (py = 0; py < hh1; py++)                                                        //  copy line pixels to outline pixels
   for (px = 0; px < ww1; px++)                                                        //    displaced by outline width
   {
      pix1 = PXBpix(pxb1,px,py);
      pix2 = PXBpix(pxb2,px+toww,py+toww);
      pix2[0] = pix1[0];
   }

   theta = 0.7 / toww;
   for (theta = 0; theta < 6.3; theta += 0.7/toww)                                     //  displace outline pixels in all directions
   {
      dx = roundf(toww * sinf(theta));
      dy = roundf(toww * cosf(theta));

      for (py = 0; py < hh1; py++)
      for (px = 0; px < ww1; px++)
      {
         pix1 = PXBpix(pxb1,px,py);
         pix2 = PXBpix(pxb2,px+toww+dx,py+toww+dy);
         if (pix2[1] < pix1[0] - pix2[0])                                              //  compare line to outline brightness
            pix2[1] = pix1[0] - pix2[0];                                               //  brighter part is outline pixel
      }
   }

   return pxb2;                                                                        //  pix2[0] / pix2[1] = line / outline
}


//  add a shadow to the line edges
//  red color [0] is original monocolor line intensity
//  green color [1] is added outline if any
//  use blue color [2] for added shadow

PXB * genline_shadow(lineattr_t *attr, PXB *pxb1)
{
   PXB         *pxb2;
   int         shww, ww1, hh1, ww2, hh2;
   int         px, py, dx, dy;
   uint8       *pix1, *pix2;
   float       theta;

   shww = attr->shwidth;                                                               //  line shadow width
   if (shww == 0) return 0;                                                            //  zero

   ww1 = pxb1->ww;                                                                     //  input PXB dimensions
   hh1 = pxb1->hh;
   ww2 = ww1 + shww * 2;                                                               //  add margins for shadow width
   hh2 = hh1 + shww * 2;
   pxb2 = PXB_make(ww2,hh2,3);                                                         //  output PXB

   for (py = 0; py < hh1; py++)                                                        //  copy line pixels to shadow pixels
   for (px = 0; px < ww1; px++)                                                        //    displaced by shadow width
   {
      pix1 = PXBpix(pxb1,px,py);
      pix2 = PXBpix(pxb2,px+shww,py+shww);
      pix2[0] = pix1[0];
      pix2[1] = pix1[1];
   }

   theta = (90 - attr->shangle) / 57.3;                                                //  degrees to radians, 0 = to the right
   dx = roundf(shww * sinf(theta));
   dy = roundf(shww * cosf(theta));

   for (py = 0; py < hh1; py++)                                                        //  displace line by shadow width
   for (px = 0; px < ww1; px++)
   {
      pix1 = PXBpix(pxb1,px,py);
      pix2 = PXBpix(pxb2,px+shww+dx,py+shww+dy);
      if (pix2[2] < pix1[0] + pix1[1] - pix2[0] - pix2[1])                             //  compare line+outline to shadow pixels
         pix2[2] = pix1[0] + pix1[1] - pix2[0] - pix2[1];                              //  brighter part is shadow pixel
   }

   return pxb2;                                                                        //  pix2[0] / pix2[1] / pix2[2]
}                                                                                      //    = line / outline / shadow brightness


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

//  draw a box around an area drag-selected with the mouse

namespace drawbox_names
{
   editfunc EFdrawbox;

   int      RGB[3];                                                                    //  box line color
   int      width;                                                                     //  box line width
   int      bx1, by1, bx2, by2;                                                        //  box NW and SE corners
}


//  menu function

void m_draw_box(GtkWidget *, ch *menu)
{
   using namespace drawbox_names;

   int drawbox_dialog_event(zdialog *zd, ch *event);
   void drawbox_mousefunc();

   printf("m_draw_box \n");

   zdialog     *zd;
   ch          *tip = TX("drag mouse to draw box \n"
                         "shift + drag center to move box \n"
                         "shift + drag edge to move edge");

   EFdrawbox.menufunc = m_draw_box;
   EFdrawbox.menuname = "Markup Box";
   EFdrawbox.Farea = 1;                                                                //  select area ignored
   EFdrawbox.mousefunc = drawbox_mousefunc;

   if (! edit_setup(EFdrawbox)) return;

/***
          ___________________________________
         |       Draw box on image           |
         | drag mouse to draw box            |
         | shift + drag center to move box   |
         | shift + drag edge to move edge    |
         |                                   |
         | line color [###]  line width [__] |
         |                                   |
         |                 [Commit] [OK] [X] |
         |___________________________________|

***/

   zd = zdialog_new(TX("Draw box on image"),Mwin,TX("Commit"),"OK","X",null);
   EFdrawbox.zd = zd;

   zdialog_add_widget(zd,"label","labtip","dialog",tip,"space=3");
   zdialog_add_widget(zd,"hbox","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbline","dialog");
   zdialog_add_widget(zd,"label","labrgb","hbline",TX("line color"),"space=3");
   zdialog_add_widget(zd,"colorbutt","RGB","hbline","255|0|0","space=3");
   zdialog_add_widget(zd,"label","space","hbline",0,"space=5");
   zdialog_add_widget(zd,"label","labwidth","hbline",TX("line width"),"space=3");
   zdialog_add_widget(zd,"zspin","width","hbline","1|20|1|1","space=3");

   zdialog_add_ttip(zd,"Commit",TX("fix box in layout and start new box"));
   
   zdialog_load_inputs(zd);
   zdialog_run(zd,drawbox_dialog_event,"save");
   zdialog_send_event(zd,"init");

   takeMouse(drawbox_mousefunc,dragcursor);
   bx1 = by1 = bx2 = by2 = 0;                                                          //  no box yet
   return;
}


//  dialog event and completion function

int drawbox_dialog_event(zdialog *zd, ch *event)
{
   using namespace drawbox_names;

   ch       color[20];
   ch       *pp;
   int      yn;

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                            //  [commit] and stay open
         zd->zstat = 0;
         PXM_free(E1pxm);                                                              //  copy edits in E3 to source image E1
         E1pxm = PXM_copy(E3pxm);
         Fpaintnow();                                                                  //  update image synchronous
         return 1;
      }

      if (zd->zstat == 2) {                                                            //  [OK] done
         if (CEF->Fmods) edit_done(0);
         else edit_cancel(0);
         m_markup(0,0);                                                                //  restart main markup dialog
         return 1;
      }      

      if (CEF->Fmods) {                                                                //  [X] cancel
         yn = zmessageYN(Mwin,TX("discard changes?"));                                 //  unsaved changes
         if (! yn) {
            zd->zstat = 0;                                                             //  back out
            return 1;
         }
      }

      edit_cancel(0);                                                                  //  cancel, no changes
      m_markup(0,0);                                                                   //  restore main markup dialog
      return 1;
   }

   if (zstrstr("init RGB width",event))                                                //  set new line color and width
   {
      zdialog_fetch(zd,"RGB",color,19);
      pp = substring(color,"|",1);
      if (pp) RGB[0] = atoi(pp);
      pp = substring(color,"|",2);
      if (pp) RGB[1] = atoi(pp);
      pp = substring(color,"|",3);
      if (pp) RGB[2] = atoi(pp);

      zdialog_fetch(zd,"width",width);
   }

   return 1;
}


//  mouse function
//  record drag start and current mouse position
//  draw box with opposing corners at these positions

void drawbox_mousefunc()
{
   using namespace drawbox_names;

   void drawline(int x1, int y1, int x2, int y2, int RGB[3], int width);

   int      mx, my, dx, dy, bump;
   int      cx, cy, rx, ry, rr;
   int      min, dt, db, dl, dr;

   if (Mxdrag || Mydrag)                                                               //  drag underway
   {
      PXM_copy(E1pxm,E3pxm);                                                           //  erase prior

      if (KBshiftkey && bx1)                                                           //  shift key, move prior box
      {
         dx = Mxdrag - Mxdown;                                                         //  new drag displacement
         dy = Mydrag - Mydown;

         Mxdown = Mxdrag;                                                              //  reset drag
         Mydown = Mydrag;
         Mxdrag = Mydrag = 0;

         mx = Mxdown;                                                                  //  mouse position
         my = Mydown;

         cx = (bx1 + bx2) / 2;                                                         //  box center
         cy = (by1 + by2) / 2;

         rx = mx - cx;                                                                 //  get mouse distance from center
         ry = my - cy;
         rr = sqrt(rx*rx + ry*ry);

         dt = abs(my - by1);                                                           //  get mouse distance from each edge
         db = abs(my - by2);                                                           //    top, bottom, left, right
         dl = abs(mx - bx1);
         dr = abs(mx - bx2);

         min = dt;                                                                     //  find minimum edge distance
         if (db < min) min = db;
         if (dl < min) min = dl;
         if (dr < min) min = dr;

         if (rr < min) {                                                               //  mouse is closer to center
            bx1 += dx;                                                                 //  move unchanged box
            by1 += dy;
            bx2 += dx;
            by2 += dy;
         }

         else {
            if (min == dt) by1 = my;                                                   //  move line closest to mouse
            if (min == db) by2 = my;
            if (min == dl) bx1 = mx;
            if (min == dr) bx2 = mx;
         }
      }

      else {                                                                           //  no shift key: create new box
         bx1 = Mxdown;
         by1 = Mydown;
         bx2 = Mxdrag;
         by2 = Mydrag;
         Mxdrag = Mydrag = 0;
      }

      bump = 1;                                                                        //  fill last pixel
      if (by2 < by1) bump = -1;                                                        //  (bx2,by2) at NW corner of pixel

      drawline(bx1,by1,bx2,by1,RGB,width);
      drawline(bx1,by2,bx2,by2,RGB,width);
      drawline(bx1,by1,bx1,by2,RGB,width);
      drawline(bx2,by1,bx2,by2+bump,RGB,width);

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

      Fpaint2();
   }

   return;
}


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

//  draw an oval around an area drag-selected with the mouse

namespace drawoval_names
{
   editfunc EFdrawoval;
   int      RGB[3];                                                                    //  oval line color
   int      width;                                                                     //  oval line width
   int      type = 1;                                                                  //  1/2 = circle/oval
   int      mx1, my1, mx2, my2;                                                        //  mouse drag coordinates
   float    exa, exb;                                                                  //  ellipse axes
   float    angle;                                                                     //  ellipse angle
   int      Fupdate;
}


//  menu function

void m_draw_oval(GtkWidget *, ch *menu)
{
   using namespace drawoval_names;

   int drawoval_dialog_event(zdialog *zd, ch *event);
   void drawoval_mousefunc();

   printf("m_draw_oval \n");

   zdialog     *zd;
   ch          *tip = TX("drag mouse down/right to draw oval \n"
                         "shift + drag center to move oval \n"
                         "shift + drag lower right edge to change");

   EFdrawoval.menufunc = m_draw_oval;
   EFdrawoval.menuname = "Markup Oval";
   EFdrawoval.Farea = 1;                                                               //  select area ignored
   EFdrawoval.mousefunc = drawoval_mousefunc;

   if (! edit_setup(EFdrawoval)) return;

/***
          _________________________________________
         |         Draw oval on image              |
         | drag mouse down/right to draw oval      |
         | shift + drag center to move oval        |
         | shift + drag lower right edge to change |
         |                                         |
         | line color [###]  line width [__]       |
         | [x] circle   [x] oval angle [__]        |
         |                                         |
         |                       [Commit] [OK] [X] |
         |_________________________________________|

***/

   zd = zdialog_new(TX("Draw oval on image"),Mwin,TX("Commit"),"OK","X",null);
   EFdrawoval.zd = zd;

   zdialog_add_widget(zd,"label","labtip","dialog",tip,"space=3");
   zdialog_add_widget(zd,"hbox","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbline","dialog");
   zdialog_add_widget(zd,"label","labline","hbline",TX("line color"),"space=3");
   zdialog_add_widget(zd,"colorbutt","RGB","hbline","255|0|0","space=3");
   zdialog_add_widget(zd,"label","space","hbline",0,"space=5");
   zdialog_add_widget(zd,"label","labwidth","hbline",TX("line width"),"space=3");
   zdialog_add_widget(zd,"zspin","width","hbline","1|20|1|1","space=3");
   zdialog_add_widget(zd,"hbox","hbco","dialog");
   zdialog_add_widget(zd,"check","circle","hbco",TX("circle"),"space=8");
   zdialog_add_widget(zd,"label","space","hbco",0,"space=10");
   zdialog_add_widget(zd,"check","oval","hbco",TX("oval"),"space=3");
   zdialog_add_widget(zd,"label","labangle","hbco",TX("angle"),"space=3");
   zdialog_add_widget(zd,"zspin","angle","hbco","0|90|1|0");

   zdialog_add_ttip(zd,"Commit",TX("fix oval in layout and start new oval"));

   zdialog_load_inputs(zd);
   zdialog_fetch(zd,"angle",angle);

   zdialog_run(zd,drawoval_dialog_event,"save");
   zdialog_send_event(zd,"init");
   takeMouse(drawoval_mousefunc,dragcursor);

   mx1 = my1 = 0;                                                                      //  no oval yet
   Fupdate = 0;

   return;
}


//  dialog event and completion function

int drawoval_dialog_event(zdialog *zd, ch *event)
{
   using namespace drawoval_names;

   void drawoval_mousefunc();

   ch       color[20];
   ch       *pp;
   int      yn;

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                            //  [commit]
         zd->zstat = 0;
         PXM_free(E1pxm);                                                              //  copy edits in E3 to source image E1
         E1pxm = PXM_copy(E3pxm);
         Fpaintnow();                                                                  //  update image synchronous
         return 1;
      }

      if (zd->zstat == 2) {                                                            //  [OK] done
         if (CEF->Fmods) edit_done(0);
         else edit_cancel(0);
         m_markup(0,0);                                                                //  restart main markup dialog
         return 1;
      }      

      if (CEF->Fmods) {                                                                //  [X] cancel
         yn = zmessageYN(Mwin,TX("discard changes?"));                                 //  unsaved changes
         if (! yn) {
            zd->zstat = 0;                                                             //  back out
            return 1;
         }
      }

      edit_cancel(0);                                                                  //  cancel, no changes
      m_markup(0,0);                                                                   //  restore main markup dialog
      return 1;
   }

   if (zstrstr("init RGB width",event))                                                //  set line color
   {
      zdialog_fetch(zd,"RGB",color,19);
      pp = substring(color,"|",1);
      if (pp) RGB[0] = atoi(pp);
      pp = substring(color,"|",2);
      if (pp) RGB[1] = atoi(pp);
      pp = substring(color,"|",3);
      if (pp) RGB[2] = atoi(pp);

      zdialog_fetch(zd,"width",width);                                                 //  line width

      zdialog_stuff(zd,"oval",0);
      zdialog_stuff(zd,"circle",0);
      if (type == 1) zdialog_stuff(zd,"circle",1);
      if (type == 2) zdialog_stuff(zd,"oval",1);
      if (zstrstr("RGB width",event)) Fupdate = 1;
   }

   if (zstrstr("oval circle",event)) {
      zdialog_stuff(zd,"oval",0);
      zdialog_stuff(zd,"circle",0);
      zdialog_stuff(zd,event,1);
      if (strmatch(event,"oval")) type = 2;
      else type = 1;
      Fupdate = 1;
   }

   if (strmatch(event,"angle")) {
      zdialog_fetch(zd,"angle",angle);
      Fupdate = 1;
   }

   if (Fupdate) drawoval_mousefunc();

   return 1;
}


//  mouse function
//  record drag start and current mouse position
//  draw oval (ellipse) with major/minor axes from drag X/Y distances.

void drawoval_mousefunc()
{
   using namespace drawoval_names;

   void * drawoval_draw(void *arg);

   int   dx, dy, rx, ry, rr;

   if (! Mxdrag && ! Mydrag && ! Fupdate) return;

   Fupdate = 0;

   if (Mxdrag || Mydrag)                                                               //  drag underway
   {
      if (KBshiftkey && mx1)                                                           //  shift key: move prior oval
      {
         dx = Mxdrag - Mxdown;                                                         //  new drag displacement
         dy = Mydrag - Mydown;

         Mxdown = Mxdrag;                                                              //  reset drag
         Mydown = Mydrag;
         Mxdrag = Mydrag = 0;

         rx = mx1 - Mxdown;                                                            //  get mouse distance from prior center
         ry = my1 - Mydown;
         rr = sqrtf(rx*rx + ry*ry);
         if (rr < 0.6 * exa || rr < 0.6 * exb) {                                       //  small, move unchanged oval
            mx1 += dx;                                                                 //  move center
            my1 += dy;
         }
         mx2 += dx;                                                                    //  move edge (change size)
         my2 += dy;
      }

      else {                                                                           //  no shift key: create new oval
         mx1 = Mxdown;
         my1 = Mydown;
         mx2 = Mxdrag;
         my2 = Mydrag;
         Mxdrag = Mydrag = 0;
      }

      exa = abs(mx2 - mx1);                                                            //  ellipse constants from
      exb = abs(my2 - my1);                                                            //    enclosing rectangle
   }

   drawoval_draw(0);                                                                   //  faster than thread
   return;
}


//  drawoval thread function - runs parallel with GTK stuff

void * drawoval_draw(void *arg)
{
   using namespace drawoval_names;

   void rotatepoint(float T, int cx, int cy, int &px, int &py);
   void drawpoint(int px, int py, int RGB[3], int W);

   int      px, py;
   float    a2, b2;
   float    x, y, x2, y2, cx, cy;
   float    T;

   PXM_copy(E1pxm,E3pxm);                                                              //  erase prior

   if (type == 1)                                                                      //  make a circle
   {
      if (exa > exb) exb = exa;                                                        //  ellipse axes are equal
      else exa = exb;

      a2 = exa * exa;
      b2 = exb * exb;
      cx = mx1;                                                                        //  center at drag origin
      cy = my1;

      for (y = -exb; y < exb; y++)                                                     //  step through y values
      {
         y2 = y * y;
         x2 = a2 * (1 - y2 / b2);
         x = sqrtf(x2);                                                                //  corresp. x values, + and -
         py = y + cy;
         px = cx - x + 0.5;
         drawpoint(px,py,RGB,width);                                                   //  draw 2 points on ellipse
         px = cx + x + 0.5;
         drawpoint(px,py,RGB,width);
      }

      for (x = -exa; x < exa; x++)                                                     //  step through x values
      {
         x2 = x * x;
         y2 = b2 * (1 - x2 / a2);
         y = sqrtf(y2);                                                                //  corresp. y values, + and -
         px = cx + x;
         py = cy - y + 0.5;
         drawpoint(px,py,RGB,width);                                                   //  draw 2 points on ellipse
         py = cy + y + 0.5;
         drawpoint(px,py,RGB,width);
      }
   }

   if (type == 2)                                                                      //  make an ellipse
   {
      a2 = exa * exa;
      b2 = exb * exb;
      cx = mx1;                                                                        //  center at drag origin
      cy = my1;

      T = PI * angle / 180;

      for (y = -exb; y < exb; y++)                                                     //  step through y values
      {
         y2 = y * y;
         x2 = a2 * (1 - y2 / b2);
         x = sqrtf(x2);                                                                //  corresp. x values, + and -
         px = cx - x + 0.5;
         py = y + cy;
         rotatepoint(T,cx,cy,px,py);
         drawpoint(px,py,RGB,width);                                                   //  draw 2 points on ellipse
         px = cx + x + 0.5;
         py = y + cy;
         rotatepoint(T,cx,cy,px,py);
         drawpoint(px,py,RGB,width);
      }

      for (x = -exa; x < exa; x++)                                                     //  step through x values
      {
         x2 = x * x;
         y2 = b2 * (1 - x2 / a2);
         y = sqrtf(y2);                                                                //  corresp. y values, + and -
         px = cx + x;
         py = cy - y + 0.5;
         rotatepoint(T,cx,cy,px,py);
         drawpoint(px,py,RGB,width);                                                   //  draw 2 points on ellipse
         px = cx + x;
         py = cy + y + 0.5;
         rotatepoint(T,cx,cy,px,py);
         drawpoint(px,py,RGB,width);
      }
   }

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

   Fpaint2();
   return 0;
}


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

//  rotate a point (px,py) around point (cx,cy) by angle T

void rotatepoint(float T, int cx, int cy, int &px, int &py)
{
   float R, T2;
   R = sqrtf((px-cx)*(px-cx) + (py-cy)*(py-cy));
   T2 = acosf((px-cx)/R);
   if (py < cy) T2 = - T2;
   T2 += T;
   px = cx + R * cosf(T2);
   py = cy + R * sinf(T2);
   return;
}


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

//  draw a line from pixel (x1,y1) to (x2,y2) with given pixel width.
//  color is RGB[3], range 0-255.

void drawline(int x1, int y1, int x2, int y2, int RGB[3], int W)
{
   void drawpoint(int px, int py, int RGB[3], int W);

   float    M;
   int      px, py, inc;

   if (abs(x1-x2) > abs(y1-y2))                                                        //  line more horizontal
   {
      M = 1.0 * (y2-y1) / (x2-x1);
      inc = 1;
      if (x2 < x1) inc = -1;
      for (px = x1; px != x2; px += inc) {                                             //  loop points in line center
         py = y1 + M * (px-x1);
         drawpoint(px,py,RGB,W);                                                       //  color center + neighbor points
      }
   }

   else
   {
      M = 1.0 * (x2-x1) / (y2-y1);                                                     //  line more vertical
      inc = 1;
      if (y2 < y1) inc = -1;
      for (py = y1; py != y2; py += inc) {
         px = x1 + M * (py-y1);
         drawpoint(px,py,RGB,W);
      }
   }

   return;
}


//  draw one 'point' of line (point size = line width)

void drawpoint(int px, int py, int RGB[3], int W)
{
   int      qx, qy, w1, w2;
   float    *pix3;

   w1 = W / 2;
   w2 = w1;
   if (!(W & 1)) w2 -= 1;

   for (qy = py-w1; qy <= py+w2; qy++)                                                 //  loop pixels overlapping point
   for (qx = px-w1; qx <= px+w2; qx++)
   {
      if (qx < 0 || qx > Eww-1) continue;
      if (qy < 0 || qy > Ehh-1) continue;
      pix3 = PXMpix(E3pxm,qx,qy);
      pix3[0] = RGB[0];
      pix3[1] = RGB[1];
      pix3[2] = RGB[2];
   }

   return;
}


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

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

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


//  menu function

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

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

   F1_help_topic = TX("COLOR MODE");

   printf("m_color_mode \n");

   EFcolormode.menuname = "Color Mode";
   EFcolormode.menufunc = m_color_mode;
   EFcolormode.threadfunc = colormode_thread;
   EFcolormode.FprevReq = 1;                                                           //  use preview
   EFcolormode.Farea = 2;                                                              //  select area usable
   EFcolormode.Fpaintedits = 1;                                                        //  allow paint edits
   EFcolormode.Fscript = 1;                                                            //  scripting supported

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

/***
          _____________________________
         |        Color Mode           |
         |                             |
         | [_] reset                   |                                               //  check boxes
         | [_] black/white positive    |
         | [_] black/white negative    |
         | [_] color negative          |
         | [_] RGB -> GBR              |
         | [_] RGB -> BRG              |
         | [_] sepia                   |
         |                             |
         |  0% ===========[]==== 100%  |
         |                             |
         |                    [OK] [X] |
         |_____________________________|

***/

   zdialog *zd = zdialog_new(TX("Color Mode"),Mwin,"OK","X",null);
   EFcolormode.zd = zd;

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

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

   return;
}


//  dialog event and completion callback function

int colormode_dialog_event(zdialog *zd, ch *event)
{
   using namespace colormode_names;

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

   if (strmatch(event,"fullsize")) {                                                   //  from select area
      edit_fullsize();                                                                 //  get full size image
      thread_signal();
      thread_wait();                                                                   //  required for paint edits
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {
         edit_fullsize();                                                              //  get full size image
         thread_signal();
         edit_addhist("mode:%d blend:%.2f",mode,mblend);                               //  edit params > edit hist
         edit_done(0);                                                                 //  commit edit
      }
      else edit_cancel(0);                                                             //  discard edit
      return 1;
   }

   if (strmatch(event,"focus")) return 1;

   if (zstrstr("reset b&wpos b&wneg colneg rgb-gbr rgb-brg sepia",event))
   {
      zdialog_stuff(zd,"reset",0);
      zdialog_stuff(zd,"b&wpos",0);
      zdialog_stuff(zd,"b&wneg",0);
      zdialog_stuff(zd,"colneg",0);
      zdialog_stuff(zd,"rgb-gbr",0);
      zdialog_stuff(zd,"rgb-brg",0);
      zdialog_stuff(zd,"sepia",0);
      zdialog_stuff(zd,event,1);
      if (strmatch(event,"reset")) mode = 0;
      if (strmatch(event,"b&wpos")) mode = 1;
      if (strmatch(event,"b&wneg")) mode = 2;
      if (strmatch(event,"colneg")) mode = 3;
      if (strmatch(event,"rgb-gbr")) mode = 4;
      if (strmatch(event,"rgb-brg")) mode = 5;
      if (strmatch(event,"sepia")) mode = 6;
   }

   zdialog_fetch(zd,"mblend",mblend);

   if (mode == 0) {
      edit_reset();
      return 1;
   }

   thread_signal();

   return 1;
}


//  thread function

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

   void * colormode_wthread(void *arg);

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

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

   do_wthreads(colormode_wthread,NSMP);                                                //  worker threads

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

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


//  worker thread functions

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

   int         index = *((int *) (arg));
   int         px, py, Fend;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       *pix1, *pix3;
   float       brite, ff1, ff2, 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 = R9 = pix3[0];
      G3 = G9 = pix3[1];
      B3 = B9 = pix3[2];

      switch (mode)
      {
         case 1: {                                                                     //  black and white positive
            R9 = G9 = B9 = 0.333 * (R1 + G1 + B1);
            break;
         }

         case 2: {                                                                     //  black and white negative
            R9 = G9 = B9 = 255.9 - 0.333 * (R1 + G1 + B1);
            break;
         }

         case 3: {                                                                     //  color negative
            R9 = 255.9 - R1;
            G9 = 255.9 - G1;
            B9 = 255.9 - B1;
            break;
         }

         case 4: {                                                                     //  RGB - GBR
            R9 = G1;
            G9 = B1;
            B9 = R1;
            break;
         }

         case 5: {                                                                     //  RGB - BRG
            R9 = B1;
            G9 = R1;
            B9 = G1;
            break;
         }

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

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

            R9 = ff1 * 255.0 + ff2 * 256;                                              //  combine max. sepia with white
            G9 = ff1 * 150.0 + ff2 * 256;
            B9 = ff1 * 46.0 + ff2 * 256;

            brite = 0.8333 * (brite + 0.2);                                            //  add brightness at low end
            R9 = R9 * brite;                                                           //  output = combined color * brightness
            G9 = G9 * brite;
            B9 = B9 * brite;
            break;
         }
      }

      RGBfix(R9,G9,B9);                                                                //  prevent underflow/overflow

      if (mblend < 1.0) {                                                              //  image blend slider
         ff1 = mblend;
         ff2 = 1.0 - ff1;
         R9 = ff1 * R9 + ff2 * R1;                                                     //  blend input and edited images
         G9 = ff1 * G9 + ff2 * G1;
         B9 = ff1 * B9 + ff2 * B1;
      }

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


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

//  Current edit function is applied to areas painted with the mouse.
//  Mouse can be weak or strong, and edits are applied incrementally.

void m_paint_edits(GtkWidget *, ch *)                                                  //  menu function
{
   int   paint_edits_dialog_event(zdialog *, ch *event);                               //  dialog event function
   void  paint_edits_mousefunc();                                                      //  mouse function

   ch    *title = TX("Paint Edits");

   F1_help_topic = TX("PAINT EDITS");

   printf("m_paint_edits \n");

   if (NFbusy) {
      printf("paint edits: function busy \n");
      return;
   }

   if (! CEF) {                                                                        //  edit func must be active
      zmessageACK(Mwin,TX("Edit function must be active"));
      return;
   }

   if (! CEF->Fpaintedits) {
      zmessageACK(Mwin,TX("Edit function can not use Paint Edits"));
      return;
   }

   if (zd_paintedits) {
      zmessageACK(Mwin,TX("Paint Edits already active"));
      return;
   }

   if (CEF->Fpreview)
      zdialog_send_event(CEF->zd,"fullsize");                                          //  use full-size image

/***
    ______________________________________
   |         Press F1 for help            |
   |                                      |
   | Mouse Radius [____]                  |
   | Power: Center [____]  Edge [____]    |
   |                                      |
   |                                [OK]  |
   |______________________________________|

***/

   zd_paintedits = zdialog_new(title,Mwin,"OK",null);
   zdialog *zd = zd_paintedits;
   zdialog_add_widget(zd,"label","labhelp1","dialog",TX("Press F1 for help"),"space=5");
   zdialog_add_widget(zd,"hbox","hbr","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labr","hbr",TX("Mouse Radius"),"space=5");
   zdialog_add_widget(zd,"zspin","radius","hbr","2|500|1|50");
   zdialog_add_widget(zd,"hbox","hbt","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labtc","hbt",TX("Power: Center"),"space=5");
   zdialog_add_widget(zd,"zspin","center","hbt","0|100|1|50");
   zdialog_add_widget(zd,"label","labte","hbt",TX("Edge"),"space=5");
   zdialog_add_widget(zd,"zspin","edge","hbt","0|100|1|0");

   Mradius = 50;
   MCpower = 50;
   MEpower = 0;

   zdialog_load_inputs(zd);
   zdialog_fetch(zd,"radius",Mradius);
   zdialog_fetch(zd,"center",MCpower);
   zdialog_fetch(zd,"edge",MEpower);

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

   Fpaintedits = 1;                                                                    //  activate mouse paint mode
   return;
}


//  dialog event and completion function

int paint_edits_dialog_event(zdialog *zd, ch *event)
{
   void  paint_edits_mousefunc();                                                      //  mouse function

   if (strmatch(event,"focus"))                                                        //  toggle mouse capture
   {
      if (CEF && CEF->Fpaintedits)                                                     //  check edit function OK
         takeMouse(paint_edits_mousefunc,0);
      else {                                                                           //  no edit function active
         freeMouse();
         return 1;
      }
   }

   if (strmatch(event,"done")) zd->zstat = 1;                                          //  from edit_done()
   if (strmatch(event,"cancel")) zd->zstat = 2;                                        //  from edit_cancel()

   if (zd->zstat)                                                                      //  done or cancel
   {
      if (CEF) zdialog_send_event(CEF->zd,"done");                                     //  complete edit
      zdialog_free(zd);                                                                //  kill dialog
      zd_paintedits = 0;
      Fpaintedits = 0;                                                                 //  paint_edits not active
      freeMouse();                                                                     //  disconnect mouse
      return 1;
   }

   if (strmatch(event,"radius"))
      zdialog_fetch(zd,"radius",Mradius);                                              //  set mouse radius

   if (strmatch(event,"center"))
      zdialog_fetch(zd,"center",MCpower);                                              //  set mouse center power 0-100

   if (strmatch(event,"edge"))
      zdialog_fetch(zd,"edge",MEpower);                                                //  set mouse edge power 0-100

   return 1;
}


//  mouse function - adjust edit strength for areas within mouse radius
//  "edge distance" is increased for more strength, decreased for less

void paint_edits_mousefunc()
{
   if (! CEF || ! CEF->Fpaintedits) {                                                  //  no active edit
      Fpaintedits = 0;
      freeMouse();                                                                     //  disconnect mouse
      return;
   }

   draw_mousecircle(Mxposn,Myposn,Mradius,0,0);                                        //  show mouse selection circle

   if (! Mdrag) return;                                                                //  wait for drag event

   Mxdrag = Mydrag = 0;                                                                //  prevent image drag

   zdialog_send_event(CEF->zd,"paint");                                                //  do edit function within mouse
   Fpaintmouse(Mxposn,Myposn,Mradius);                                                 //  update drawing window
   draw_mousecircle(Mxposn,Myposn,Mradius,0,0);                                        //  refresh mouse selection circle

   return;
}


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

//  Pixel paint function - paint individual pixels with the mouse.
//  The mouse circle paints a selected color.

namespace paint_image_names
{
   int   paint_dialog_event(zdialog* zd, ch *event);
   void  paint_mousefunc();
   void  paint_dopixels(int px, int py);                                               //  update pixel block
   void  paint_savepixB(int px, int py);                                               //  save pixel block for poss. undo
   void  paint_undolastB();                                                            //  undo last pixel block, free memory
   void  paint_freefirstB();                                                           //  free memory for first pixel block
   void  paint_freeallB();                                                             //  free memory for all pixel blocks

   uint8    RGB[3] = { 0, 0, 0 };                                                      //  color to paint
   int      mode;                                                                      //  1/2 = paint / erase
   int      Mrad;                                                                      //  mouse radius
   int      Fptran = 0;                                                                //  flag, paint over transparent areas
   int      Fdrag = 0;                                                                 //  flag, mouse drags image
   int      nc, ac;                                                                    //  no. channels, alpha channel
   float    kernel[202][202];                                                          //  Mrad <= 100

   int64    maxmem = (int64) 4000 * MEGA;                                              //  max. pixel block memory (4 GB)
   int64    totmem;                                                                    //  pixB memory allocated
   int      maxpixB = 10000;                                                           //  max. pixel blocks
   int      totpixB = 0;                                                               //  total pixel blocks
   int      pixBseq = 0;                                                               //  last pixel block sequence no.

   typedef struct {                                                                    //  pixel block before edit
      int         seq;                                                                 //  block sequence no.
      uint16      px, py;                                                              //  center pixel (radius org.)
      uint16      radius;                                                              //  radius of pixel block
      float       pixel[][4];                                                          //  array of pixel[npix][4]
   }  pixBmem_t;

   pixBmem_t   **pixBmem = 0;                                                          //  *pixBmem_t[]

   int   pixBmem_cc = 12;                                                              //  all except pixel array + pad
   int   pcc4 = 4 * sizeof(float);                                                     //  pixel cc: RGBA = 4 channels

   editfunc    EFpaint;
}


//  menu function

void m_paint_image(GtkWidget *, ch *menu)                                              //  separate paint and copy
{
   using namespace paint_image_names;

   ch       *mess1 = TX("shift + left click: pick color from image \n"
                        "left drag: paint color on image \n"                           //  remove click actions
                        "right drag: restore original image");
   ch       *dash = "  \xcc\xb6 ";

   F1_help_topic = TX("PAINT IMAGE");

   printf("m_paint_image \n");

   EFpaint.menufunc = m_paint_image;
   EFpaint.menuname = "Paint Image";
   EFpaint.Farea = 2;                                                                  //  select area usable
   EFpaint.mousefunc = paint_mousefunc;                                                //  mouse function

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

   /****
             __________________________________________________
            |                 Paint on Image                   |
            |                                                  |
            |  shift + left click: pick color from image       |
            |  left drag: paint color on image                 |
            |  right drag: restore original image              |
            |                                                  |
            |  paint color [######]     [palette]   [HSL]      |
            |                                                  |
            |  brush size      NN ============[]=============  |
            |  opacity center  NN ==================[]=======  |
            |  opacity edge    NN ====[]=====================  |
            |                                                  |
            |  (o) paint  (o) erase   [undo last]  [undo all]  |
            |  [x] include transparent areas                   |
            |  [x] drag image     zoom image [+] [-]           |
            |                                                  |
            |                                         [OK] [X] |
            |__________________________________________________|

   ****/

   zdialog *zd = zdialog_new(TX("Paint on Image"),Mwin,"OK","X",null);
   EFpaint.zd = zd;

   zdialog_add_widget(zd,"label","labm","dialog",mess1,"space=5");

   zdialog_add_widget(zd,"hbox","hbp","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labp","hbp",TX("paint color"),"space=5");
   zdialog_add_widget(zd,"colorbutt","colorbutt","hbp","255|0|0");
   zdialog_add_widget(zd,"label","space","hbp",0,"space=10");
   zdialog_add_widget(zd,"button","palette","hbp",TX("palette"),"space=10");
   zdialog_add_widget(zd,"button","HSL","hbp","HSL");

   zdialog_add_widget(zd,"hbox","hbbru","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbbru1","hbbru",0,"homog|space=1");
   zdialog_add_widget(zd,"vbox","vbbru2","hbbru",0,"homog|space=1");
   zdialog_add_widget(zd,"vbox","vbbru3","hbbru",0,"homog|expand|space=1");

   zdialog_add_widget(zd,"label","labbr","vbbru1",TX("brush size"));
   zdialog_add_widget(zd,"label","laboc","vbbru1",TX("opacity center"));
   zdialog_add_widget(zd,"label","laboe","vbbru1",TX("opacity edge"));

   zdialog_add_widget(zd,"label","labbrNN","vbbru2","NN");
   zdialog_add_widget(zd,"label","labocNN","vbbru2","NNN");
   zdialog_add_widget(zd,"label","laboeNN","vbbru2","NNN");

   zdialog_add_widget(zd,"hscale","Mrad","vbbru3","1|100|1|20","expand");
   zdialog_add_widget(zd,"hscale","opccent","vbbru3","1|100|1|50","expand");
   zdialog_add_widget(zd,"hscale","opcedge","vbbru3","0|100|1|100","expand");

   zdialog_add_widget(zd,"hbox","hbp","dialog",0,"space=3");
   zdialog_add_widget(zd,"radio","paint","hbp",TX("paint"),"space=3");
   zdialog_add_widget(zd,"radio","erase","hbp",TX("erase"));
   zdialog_add_widget(zd,"button","undlast","hbp",TX("Undo Last"),"space=5");
   zdialog_add_widget(zd,"button","undall","hbp",TX("Undo All"));

   zdialog_add_widget(zd,"hbox","hbt","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","Fptran","hbt",TX("include transparent areas"),"space=3");

   zdialog_add_widget(zd,"hbox","hbd","dialog");
   zdialog_add_widget(zd,"check","Fdrag","hbd",TX("drag image"),"space=3");
   zdialog_add_widget(zd,"label","space","hbd",0,"space=10");
   zdialog_add_widget(zd,"label","labzoom","hbd",TX("zoom image"),"space=3");
   zdialog_add_widget(zd,"button","zoom+","hbd"," + ","space=3");
   zdialog_add_widget(zd,"button","zoom-","hbd",dash,"space=3");

   zdialog_rescale(zd,"Mrad",1,2,100);                                                 //  stretch scales at sensitive end
   zdialog_rescale(zd,"opccent",1,2,100);
   zdialog_rescale(zd,"opcedge",0,1,100);

   zdialog_load_inputs(zd);                                                            //  preload prior user inputs

   zdialog_stuff(zd,"Fptran",0);                                                       //  initialize
   zdialog_stuff(zd,"paint",1);
   zdialog_stuff(zd,"erase",0);
   zdialog_stuff(zd,"Fdrag",0);
   mode = 1;
   Fdrag = 0;

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

   zdialog_send_event(zd,"colorbutt");                                                 //  initialize paint color
   zdialog_send_event(zd,"Mrad");                                                      //  get kernel initialized
   zdialog_fetch(zd,"Fptran",Fptran);                                                  //  paint over transparent areas

   totmem = 0;                                                                         //  memory used
   pixBmem = 0;                                                                        //  pixel block memory
   totpixB = 0;                                                                        //  pixel blocks
   pixBseq = 0;

   ac = 0;
   nc = E1pxm->nc;                                                                     //  channels, RGBA
   if (nc > 3) ac = 1;                                                                 //  alpha channel present

   takeMouse(paint_mousefunc,drawcursor);                                              //  connect mouse function
   return;
}


//  dialog event and completion callback function

int paint_image_names::paint_dialog_event(zdialog *zd, ch *event)
{
   using namespace paint_image_names;

   ch          *pp;
   ch          color[20], text[20];
   float       opccent, opcedge;                                                       //  center and edge opacities
   int         paint, radius, dx, dy, err;
   float       rad, kern;

   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
      paint_freeallB();                                                                //  free pixel block memory
      return 1;
   }

   draw_mousecircle(0,0,0,1,0);                                                        //  erase mouse circle

   if (strmatch(event,"focus"))                                                        //  toggle mouse capture
      takeMouse(paint_mousefunc,drawcursor);

   if (strmatch(event,"colorbutt"))
   {
      zdialog_fetch(zd,"colorbutt",color,19);                                          //  get paint color from color button
      pp = substring(color,"|",1);
      if (pp) RGB[0] = atoi(pp);
      pp = substring(color,"|",2);
      if (pp) RGB[1] = atoi(pp);
      pp = substring(color,"|",3);
      if (pp) RGB[2] = atoi(pp);
   }

   if (strmatch(event,"palette"))
      err = RGB_chooser(zd,"colorbutt",RGB);                                           //  select color from palette

   if (strmatch(event,"HSL")) {
      err = HSL_chooser(zd,"colorbutt",RGB);                                           //  select color from palette
      if (err) return 1;
      snprintf(color,20,"%d|%d|%d",RGB[0],RGB[1],RGB[2]);
      zdialog_stuff(zd,"colorbutt",color);
   }

   if (zstrstr("Mrad opccent opcedge",event))
   {
      zdialog_fetch(zd,"Mrad",Mrad);                                                   //  get new brush attributes
      zdialog_fetch(zd,"opccent",opccent);
      zdialog_fetch(zd,"opcedge",opcedge);

      sprintf(text,"%3d",Mrad);                                                        //  stuff corresp. number values
      zdialog_stuff(zd,"labbrNN",text);
      sprintf(text,"%3.0f",opccent);
      zdialog_stuff(zd,"labocNN",text);
      sprintf(text,"%3.0f",opcedge);
      zdialog_stuff(zd,"laboeNN",text);

      opccent = 0.01 * opccent;                                                        //  opacity  0 ... 1
      opcedge = 0.01 * opcedge;
      opccent = pow(opccent,2.0);                                                      //  change response curve
      opcedge = opccent * opcedge;                                                     //  edge relative to center

      radius = Mrad;

      for (dy = -radius; dy <= radius; dy++)                                           //  build kernel
      for (dx = -radius; dx <= radius; dx++)
      {
         rad = sqrt(dx*dx + dy*dy);
         kern = (radius - rad) / radius;                                               //  center ... edge  >>  1 ... 0
         kern = kern * (opccent - opcedge) + opcedge;                                  //  opacity  center ... edge
         if (rad > radius) kern = 0;                                                   //  beyond radius, within square
         if (kern < 0) kern = 0;
         if (kern > 1) kern = 1;
         kernel[dx+radius][dy+radius] = kern;
      }
   }

   if (strmatch(event,"undlast"))                                                      //  undo last edit (click or drag)
      paint_undolastB();

   if (strmatch(event,"undall")) {                                                     //  undo all edits
      edit_reset();
      paint_freeallB();
   }

   if (strmatch(event,"Fptran"))                                                       //  flag, paint over transparency
      zdialog_fetch(zd,"Fptran",Fptran);

   if (zstrstr("paint erase",event)) {                                                 //  set paint or erase mode
      zdialog_fetch(zd,"paint",paint);
      if (paint) mode = 1;
      else mode = 2;
   }

   if (strmatch(event,"Fdrag"))                                                        //  mouse drags image
      zdialog_fetch(zd,"Fdrag",Fdrag);

   if (strmatch(event,"zoom+")) m_zoom(0,"in");                                        //  zoom image in or out
   if (strmatch(event,"zoom-")) m_zoom(0,"out");

   return 1;
}


//  mouse function

void paint_image_names::paint_mousefunc()                                              //  no action on clicks
{
   using namespace paint_image_names;

   static int  pmxdown = 0, pmydown = 0;
   static int  pmxdrag = 0, pmydrag = 0;
   ch          color[20];
   float       *pix3;
   zdialog     *zd = EFpaint.zd;

   if (Fdrag) return;                                                                  //  pass through to main()

   if (LMclick && KBshiftkey)                                                          //  shift + left mouse click
   {
      LMclick = 0;
      pix3 = PXMpix(E3pxm,Mxclick,Myclick);                                            //  pick new color from image
      RGB[0] = pix3[0];
      RGB[1] = pix3[1];
      RGB[2] = pix3[2];
      snprintf(color,19,"%d|%d|%d",RGB[0],RGB[1],RGB[2]);
      if (zd) zdialog_stuff(zd,"colorbutt",color);
      return;
   }

   if (Mxdrag || Mydrag)                                                               //  drag in progress
   {
      if (Mxdown != pmxdown || Mydown != pmydown) {                                    //  new drag
         pixBseq++;                                                                    //  new undo seq. no.
         pmxdown = Mxdown;
         pmydown = Mydown;
      }

      if (Mxdrag == pmxdrag && Mydrag == pmydrag) {                                    //  no movement
         Mxdrag = Mydrag = 0;
         return;
      }

      pmxdrag = Mxdrag;
      pmydrag = Mydrag;

      paint_dopixels(Mxdrag,Mydrag);                                                   //  do 1 block of pixels
   }

   draw_mousecircle(Mxposn,Myposn,Mrad,0,0);                                           //  draw mouse circle

   Mxdrag = Mydrag = LMclick = 0;
   return;
}


//  paint or erase 1 block of pixels within mouse radius of px, py

void paint_image_names::paint_dopixels(int px, int py)
{
   using namespace paint_image_names;

   float       *pix1, *pix3;
   int         radius, dx, dy, qx, qy;
   int         ii, ww, hh, dist = 0;
   int         pot = ac * Fptran;                                                      //  paint over transparent areas
   float       red, green, blue;
   float       kern;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   draw_mousecircle(0,0,0,1,cr);                                                       //  erase mouse circle

   ww = Eww;
   hh = Ehh;

   paint_savepixB(px,py);                                                              //  save pixels for poss. undo

   red = RGB[0];                                                                       //  paint color
   green = RGB[1];
   blue = RGB[2];

   radius = Mrad;

   if (mode == 1) {                                                                    //  paint
      CEF->Fmods++;
      CEF->Fsaved = 0;
   }

   for (dy = -radius; dy <= radius; dy++)                                              //  loop surrounding block of pixels
   for (dx = -radius; dx <= radius; dx++)
   {
      qx = px + dx;
      qy = py + dy;

      if (qx < 0 || qx > ww-1) continue;
      if (qy < 0 || qy > hh-1) continue;

      if (sa_stat == sa_stat_fini) {                                                   //  select area active
         ii = qy * ww + qx;
         dist = sa_pixmap[ii];
         if (! dist) continue;                                                         //  pixel is outside area
      }

      kern = kernel[dx+radius][dy+radius];                                             //  mouse opacities
      if (kern == 0) continue;                                                         //  outside mouse radius

      pix1 = PXMpix(E1pxm,qx,qy);                                                      //  source image pixel
      pix3 = PXMpix(E3pxm,qx,qy);                                                      //  edited image pixel

      if (mode == 1 && Mbutton < 2)                                                    //  paint
      {
         kern = kern * (1.0 + 50.0 * wacom_pressure);                                  //  wacom
         if (kern > 1.0) kern = 1.0;
         pix3[0] = kern * red   + (1.0 - kern) * pix3[0];                              //  overpaints accumulate
         pix3[1] = kern * green + (1.0 - kern) * pix3[1];
         pix3[2] = kern * blue  + (1.0 - kern) * pix3[2];
         if (pot) pix3[3] = kern * 255.0  + (1.0 - kern) * pix3[3];                    //  overpaint transparent area
      }

      if (mode == 2 || Mbutton >= 2)                                                   //  erase
      {
         kern = kern * (2.0 + 50.0 * wacom_pressure);                                  //  wacom
         if (kern > 1.0) kern = 1.0;
         if (kern > 1) kern = 1;
         pix3[0] = kern * pix1[0] + (1.0 - kern) * pix3[0];                            //  gradual erase
         pix3[1] = kern * pix1[1] + (1.0 - kern) * pix3[1];
         pix3[2] = kern * pix1[2] + (1.0 - kern) * pix3[2];
         if (pot) pix3[3] = kern * pix1[3] + (1.0 - kern) * pix3[3];
      }
   }

   px = px - radius - 1;                                                               //  repaint modified area
   py = py - radius - 1;
   ww = 2 * radius + 3;
   Fpaint3(px,py,ww,ww,cr);

   draw_context_destroy(draw_context);
   return;
}


//  save 1 block of pixels for possible undo

void paint_image_names::paint_savepixB(int px, int py)
{
   using namespace paint_image_names;

   int            cc, npix, radius, dx, dy;
   float          *pix3;
   pixBmem_t      *paintsave1;

   if (! pixBmem) {                                                                    //  first time
      cc = maxpixB * sizeof(void *);
      pixBmem = (pixBmem_t **) zmalloc(cc,"paint image");
      totpixB = 0;
      totmem = 0;
   }

   if (totmem > maxmem || totpixB == maxpixB)                                          //  free memory for oldest updates
      while (totmem > 0.7 * maxmem || totpixB > 0.7 * maxpixB)
         paint_freefirstB();

   radius = Mrad;
   npix = 0;

   for (dy = -radius; dy <= radius; dy++)                                              //  count pixels in block
   for (dx = -radius; dx <= radius; dx++)
   {
      if (px + dx < 0 || px + dx > Eww-1) continue;
      if (py + dy < 0 || py + dy > Ehh-1) continue;
      npix++;
   }

   cc = npix * pcc4 + pixBmem_cc;
   paintsave1 = (pixBmem_t *) zmalloc(cc,"paint image");                               //  allocate memory for block
   pixBmem[totpixB] = paintsave1;
   totpixB += 1;
   totmem += cc;

   paintsave1->seq = pixBseq;                                                          //  save pixel block poop
   paintsave1->px = px;
   paintsave1->py = py;
   paintsave1->radius = radius;

   npix = 0;

   for (dy = -radius; dy <= radius; dy++)                                              //  save pixels in block
   for (dx = -radius; dx <= radius; dx++)
   {
      if (px + dx < 0 || px + dx > Eww-1) continue;
      if (py + dy < 0 || py + dy > Ehh-1) continue;
      pix3 = PXMpix(E3pxm,(px+dx),(py+dy));                                            //  edited image pixel
      paintsave1->pixel[npix][0] = pix3[0];
      paintsave1->pixel[npix][1] = pix3[1];
      paintsave1->pixel[npix][2] = pix3[2];
      if (ac) paintsave1->pixel[npix][3] = pix3[3];
      npix++;
   }

   return;
}


//  undo last pixel block (newest edit) and free memory

void paint_image_names::paint_undolastB()
{
   using namespace paint_image_names;

   int            ii, cc, npix, radius;
   int            ww, px, py, dx, dy;
   float          *pix3;
   pixBmem_t      *paintsave1;

   for (ii = totpixB-1; ii >= 0; ii--)
   {
      paintsave1 = pixBmem[ii];
      if (paintsave1->seq != pixBseq) break;
      px = paintsave1->px;
      py = paintsave1->py;
      radius = paintsave1->radius;

      npix = 0;
      for (dy = -radius; dy <= radius; dy++)
      for (dx = -radius; dx <= radius; dx++)
      {
         if (px + dx < 0 || px + dx > Eww-1) continue;
         if (py + dy < 0 || py + dy > Ehh-1) continue;
         pix3 = PXMpix(E3pxm,(px+dx),(py+dy));
         pix3[0] = paintsave1->pixel[npix][0];
         pix3[1] = paintsave1->pixel[npix][1];
         pix3[2] = paintsave1->pixel[npix][2];
         if (ac) pix3[3] = paintsave1->pixel[npix][3];
         npix++;
      }

      px = px - radius - 1;
      py = py - radius - 1;
      ww = 2 * radius + 3;
      Fpaint3(px,py,ww,ww,0);

      zfree(paintsave1);
      pixBmem[ii] = 0;
      cc = npix * pcc4 + pixBmem_cc;
      totmem -= cc;
      totpixB--;
   }

   if (pixBseq > 0) --pixBseq;
   return;
}


//  free memory for first pixel block (oldest edit)

void paint_image_names::paint_freefirstB()
{
   using namespace paint_image_names;

   int            firstseq;
   int            ii, jj, cc, npix, radius;
   int            px, py, dx, dy;
   pixBmem_t      *paintsave1;

   if (! totpixB) return;
   firstseq = pixBmem[0]->seq;

   for (ii = 0; ii < totpixB; ii++)
   {
      paintsave1 = pixBmem[ii];
      if (paintsave1->seq != firstseq) break;
      px = paintsave1->px;
      py = paintsave1->py;
      radius = paintsave1->radius;
      npix = 0;
      for (dy = -radius; dy <= radius; dy++)
      for (dx = -radius; dx <= radius; dx++)
      {
         if (px + dx < 0 || px + dx > Eww-1) continue;
         if (py + dy < 0 || py + dy > Ehh-1) continue;
         npix++;
      }

      zfree(paintsave1);
      pixBmem[ii] = 0;
      cc = npix * pcc4 + pixBmem_cc;
      totmem -= cc;
   }

   for (jj = 0; ii < totpixB; jj++, ii++)
      pixBmem[jj] = pixBmem[ii];

   totpixB = jj;
   return;
}


//  free all pixel block memory

void paint_image_names::paint_freeallB()
{
   using namespace paint_image_names;

   int            ii;
   pixBmem_t      *paintsave1;

   for (ii = totpixB-1; ii >= 0; ii--)
   {
      paintsave1 = pixBmem[ii];
      zfree(paintsave1);
   }

   if (pixBmem) zfree(pixBmem);
   pixBmem = 0;

   pixBseq = 0;
   totpixB = 0;
   totmem = 0;

   return;
}


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

//  Select a color from a color chooser image file.
//  Returns color button in callers zdialog.
//  Returns selected RGB color in prgb[3] argument.

namespace RGB_chooser_names
{
// ch          *RGB_chooser_file;                                                      //  defined in fotocx.h
   ch          color[20];
   zdialog     *pzdialog;
   ch          *pcolorbutt;
   uint8       *pRGB;
   PIXBUF      *pixbuf = 0;
   GtkWidget   *frame, *drawarea;
}


int RGB_chooser(zdialog *pzd, ch *pbutt, uint8 prgb[3])
{
   using namespace RGB_chooser_names;

   int   RGB_chooser_dialog_event(zdialog *zd, ch *event);                             //  dialog events function
   int   RGB_chooser_draw(GtkWidget *window, cairo_t *cr);                             //  window draw function
   int   RGB_chooser_mouse(GtkWidget *window, GdkEventButton *);                       //  window mouse button event

   pzdialog = pzd;                                                                     //  copy args from caller
   pcolorbutt = pbutt;
   pRGB = prgb;

   if (! RGB_chooser_file || ! regfile(RGB_chooser_file)) {                            //  default color chooser file
      RGB_chooser_file = (ch *) zmalloc(200,"color chooser");
      snprintf(RGB_chooser_file,200,"%s/CIE1931.jpg",get_zdatadir());
   }

   /***
          ____________________________________________
         |             Color Chooser                  |
         |                                            |
         |          click on desired color            |
         |  ________________________________________  |
         | |                                        | |
         | |                                        | |
         | |                                        | |
         | |                  image                 | |
         | |                                        | |
         | |                                        | |
         | |                                        | |
         | |                                        | |
         | |________________________________________| |
         |                                            |
         | [_______________________________] [Browse] |
         |                                            |
         |                                        [X] |
         |____________________________________________|

   ***/

   zdialog *zd = zdialog_new(TX("Color Chooser"),Mwin,"X",null);

   zdialog_add_widget(zd,"label","labclick","dialog",TX("click on desired color"));

   zdialog_add_widget(zd,"frame","frame","dialog");
   frame = zdialog_gtkwidget(zd,"frame");
   drawarea = gtk_drawing_area_new();
   gtk_widget_set_size_request(drawarea,-1,200);
   gtk_container_add(GTK_CONTAINER(frame),drawarea);

   zdialog_add_widget(zd,"hbox","hbfile","dialog",0,"space=3");
   zdialog_add_widget(zd,"zentry","file","hbfile",0,"space=3|expand");
   zdialog_add_widget(zd,"button","browse","hbfile",TX("Browse"),"space=3");
   zdialog_stuff(zd,"file",RGB_chooser_file);

   G_SIGNAL(drawarea,"draw",RGB_chooser_draw,0);
   G_SIGNAL(drawarea,"button-press-event",RGB_chooser_mouse,0);
   gtk_widget_add_events(drawarea,GDK_BUTTON_PRESS_MASK);

   zdialog_resize(zd,300,0);
   zdialog_run(zd,RGB_chooser_dialog_event,"save");

   return 0;
}


//  dialog event and completion function

int RGB_chooser_dialog_event(zdialog *zd, ch *event)
{
   using namespace RGB_chooser_names;

   ch       *pp;

   if (strmatch(event,"browse"))
   {
      pp = navi::galleryname;
      if (! pp) pp = topfolders[0];
      pp = select_files1(pp);
      if (! pp) return 1;
      zdialog_stuff(zd,"file",pp);
      if (RGB_chooser_file) zfree(RGB_chooser_file);
      RGB_chooser_file = pp;
      gtk_widget_queue_draw(drawarea);
   }

   if (zd->zstat) zdialog_free(zd);
   return 1;
}


//  color chooser window draw function

int RGB_chooser_draw(GtkWidget *widget, cairo_t *cr)
{
   using namespace RGB_chooser_names;

   PIXBUF      *pixbuf1;
   GError      *gerror = 0;
   GdkWindow   *gdkwin;
   int         ww1, hh1, ww2, hh2;

   if (*RGB_chooser_file != '/') return 1;                                             //  load last color chooser file

   pixbuf1 = gdk_pixbuf_new_from_file_at_size(RGB_chooser_file,500,500,&gerror);
   if (! pixbuf1) {
      printf("*** pixbuf error: %s \n",gerror->message);                               //  popup message >> draw event loop
      return 1;
   }

   ww1 = gdk_pixbuf_get_width(pixbuf1);                                                //  image dimensions
   hh1 = gdk_pixbuf_get_height(pixbuf1);

   gdkwin = gtk_widget_get_window(widget);                                             //  set drawing area to match
   ww2 = gdk_window_get_width(gdkwin);                                                 //    aspect ratio
   hh2 = ww2 * hh1 / ww1;
   gtk_widget_set_size_request(widget,-1,hh2);

   if (pixbuf) g_object_unref(pixbuf);
   pixbuf = gdk_pixbuf_scale_simple(pixbuf1,ww2,hh2,BILINEAR);
   g_object_unref(pixbuf1);
   if (! pixbuf) return 1;

   gdk_cairo_set_source_pixbuf(cr,pixbuf,0,0);                                         //  draw image
   cairo_paint(cr);

   return 1;
}


//  color chooser mouse click function

int RGB_chooser_mouse(GtkWidget *widget, GdkEventButton *event)
{
   using namespace RGB_chooser_names;

   int      mx, my, rs, nc;
   uint8    *pixels, *pix1;

   if (! pixbuf) return 1;

   rs = gdk_pixbuf_get_rowstride(pixbuf);
   nc = gdk_pixbuf_get_n_channels(pixbuf);
   pixels = gdk_pixbuf_get_pixels(pixbuf);

   mx = event->x;
   my = event->y;
   pix1 = pixels + my * rs + mx * nc;

   snprintf(color,20,"%d|%d|%d",pix1[0],pix1[1],pix1[2]);                              //  update caller's color button
   zdialog_stuff(pzdialog,pcolorbutt,color);

   pRGB[0] = pix1[0];                                                                  //  update caller's paint color
   pRGB[1] = pix1[1];
   pRGB[2] = pix1[2];

   return 1;
}


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

//  HSL color chooser function
//  Returns color button in callers zdialog.
//  Returns selected RGB color in prgb[3] argument.

namespace HSL_chooser_names
{
   zdialog     *HSLzdialog;
   GtkWidget   *RGBframe, *RGBcolor;
   GtkWidget   *Hframe, *Hscale;
   zdialog     *pzdialog;
   ch          *pcolorbutt;
   uint8       *pRGB;
   float       H, S, L;                                                                //  chosen HSL color
   float       R, G, B;                                                                //  corresp. RGB color
}

int HSL_chooser(zdialog *pzd, ch *pbutt, uint8 prgb[3])
{
   using namespace HSL_chooser_names;

   void   HSL_chooser_RGB(GtkWidget *drawarea, cairo_t *cr, int *);
   void   HSL_chooser_Hscale(GtkWidget *drawarea, cairo_t *cr, int *);
   int    HSL_chooser_dialog_event(zdialog *zd, ch *event);

   pzdialog = pzd;                                                                     //  copy args from caller
   pcolorbutt = pbutt;
   pRGB = prgb;

/***
       ________________________________________________
      |                                                |
      |  [#######]   [##############################]  |
      |  Color Hue   ================[]==============  |
      |  Saturation  =====================[]=========  |
      |  Lightness   ===========[]===================  |
      |                                                |
      |                                            [X] |
      |________________________________________________|

***/

   zdialog *zd = zdialog_new(TX("Adjust HSL"),Mwin,"X",null);
   HSLzdialog = zd;

   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb2",0,"homog|space=0");
   zdialog_add_widget(zd,"vbox","vb2","hb2",0,"homog|expand|space=0");

   zdialog_add_widget(zd,"frame","RGBframe","vb1",0,"space=1");                        //  drawing area for RGB color
   RGBframe = zdialog_gtkwidget(zd,"RGBframe");
   RGBcolor = gtk_drawing_area_new();
   gtk_container_add(GTK_CONTAINER(RGBframe),RGBcolor);
   gtk_widget_set_size_request(RGBcolor,0,16);
   G_SIGNAL(RGBcolor,"draw",HSL_chooser_RGB,0);

   zdialog_add_widget(zd,"frame","Hframe","vb2",0,"space=1");                          //  drawing area for hue scale
   Hframe = zdialog_gtkwidget(zd,"Hframe");
   Hscale = gtk_drawing_area_new();
   gtk_container_add(GTK_CONTAINER(Hframe),Hscale);
   gtk_widget_set_size_request(Hscale,200,16);
   G_SIGNAL(Hscale,"draw",HSL_chooser_Hscale,0);

   zdialog_add_widget(zd,"label","labhue","vb1",TX("Color Hue"));
   zdialog_add_widget(zd,"label","labsat","vb1",TX("Saturation"));
   zdialog_add_widget(zd,"label","lablgt","vb1",TX("Lightness"));

   zdialog_add_widget(zd,"hscale","H","vb2","0|359.9|0.1|180","expand");
   zdialog_add_widget(zd,"hscale","S","vb2","0|1|0.001|0.5","expand");
   zdialog_add_widget(zd,"hscale","L","vb2","0|1|0.001|0.5","expand");

   H = 180;                                                                            //  chosen HSL color = not set
   S = 0.5;
   L = 0.5;

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


//  Paint RGBcolor drawing area with RGB color from chosen HSL color

void HSL_chooser_RGB(GtkWidget *drawarea, cairo_t *cr, int *)
{
   using namespace HSL_chooser_names;

   int      ww, hh;
   int      r, g, b;
   ch       color[20];

   ww = gtk_widget_get_allocated_width(drawarea);                                      //  drawing area size
   hh = gtk_widget_get_allocated_height(drawarea);

   HSLtoRGB(H,S,L,R,G,B);                                                              //  RGB color from chosen HSL

   cairo_set_source_rgb(cr,R,G,B);
   cairo_rectangle(cr,0,0,ww-1,hh-1);
   cairo_fill(cr);

   r = 255 * R;
   g = 255 * G;
   b = 255 * B;

   snprintf(color,20,"%d|%d|%d",r,g,b);                                                //  update caller's color button
   zdialog_stuff(pzdialog,pcolorbutt,color);

   pRGB[0] = r;                                                                        //  update caller's paint color
   pRGB[1] = g;
   pRGB[2] = b;

   return;
}


//  Paint Hscale drawing area with all hue values in a horizontal scale

void HSL_chooser_Hscale(GtkWidget *drawarea, cairo_t *cr, int *)
{
   using namespace HSL_chooser_names;

   int      px, ww, hh;
   float    H, S, L, R, G, B;

   ww = gtk_widget_get_allocated_width(drawarea);                                      //  drawing area size
   hh = gtk_widget_get_allocated_height(drawarea);

   S = L = 0.5;

   for (px = 0; px < ww; px++)                                                         //  paint hue color scale
   {
      H = 360 * px / ww;
      HSLtoRGB(H,S,L,R,G,B);
      cairo_set_source_rgb(cr,R,G,B);
      cairo_move_to(cr,px,0);
      cairo_line_to(cr,px,hh-1);
      cairo_stroke(cr);
   }

   return;
}


//  HSL dialog event and completion function

int HSL_chooser_dialog_event(zdialog *zd, ch *event)                                   //  HSL dialog event function
{
   using namespace HSL_chooser_names;

   if (zd->zstat) {                                                                    //  zdialog complete
      zdialog_free(zd);
      freeMouse();
      HSLzdialog = 0;
      return 1;
   }

   if (zstrstr("H S L",event)) {                                                       //  HSL inputs changed
      zdialog_fetch(zd,"H",H);
      zdialog_fetch(zd,"S",S);
      zdialog_fetch(zd,"L",L);
      gtk_widget_queue_draw(RGBcolor);                                                 //  draw corresp. RGB color
   }

   return 1;
}


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

//  HSL to RGB converter (Wikipedia)
//  H = 0-360 deg.  S = 0-1   L = 0-1
//  output RGB values = 0-1

void HSLtoRGB(float H, float S, float L, float &R, float &G, float &B)
{
   float    C, X, M;
   float    h1, h2;

   h1 = H / 60;
   h2 = h1 - 2 * int(h1/2);

   C = (1 - fabsf(2*L-1)) * S;
   X = C * (1 - fabsf(h2-1));
   M = L - C/2;

   if (H < 60) {
      R = C;
      G = X;
      B = 0;
   }

   else if (H < 120) {
      R = X;
      G = C;
      B = 0;
   }

   else if (H < 180) {
      R = 0;
      G = C;
      B = X;
   }

   else if (H < 240) {
      R = 0;
      G = X;
      B = C;
   }

   else if (H < 300) {
      R = X;
      G = 0;
      B = C;
   }

   else {
      R = C;
      G = 0;
      B = X;
   }

   R = R + M;
   G = G + M;
   B = B + M;

   if (R < 0) R = 0;
   if (G < 0) G = 0;
   if (B < 0) B = 0;

   if (R > 255.0) R = 255.0;
   if (G > 255.0) G = 255.0;
   if (B > 255.0) B = 255.0;

   return;
}


//  RGB to HSL converter
//  input RGB values 0-1
//  outputs: H = 0-360 deg.  S = 0-1   L = 0-1

void RGBtoHSL(float R, float G, float B, float &H, float &S, float &L)
{
   float    max, min, D;

   max = R;
   if (G > max) max = G;
   if (B > max) max = B;

   min = R;
   if (G < min) min = G;
   if (B < min) min = B;

   D = max - min;

   L = 0.5 * (max + min);

   if (D < 0.004) {
      H = S = 0;
      return;
   }

   if (L > 0.5)
      S = D / (2 - max - min);
   else
      S = D / (max + min);

   if (max == R)
      H = (G - B) / D;
   else if (max == G)
      H = 2 + (B - R) / D;
   else
      H = 4 + (R - G) / D;

   H = H * 60;
   if (H < 0) H += 360;

   return;
}


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

//  Paint transparency function - paint pixel alpha channel with the mouse.

namespace paint_transp_names
{
   int      mode = 1;                                                                  //  1/2 = more/less transparency
   int      Fgrad = 1;                                                                 //  gradual or instant transparency
   int      Mrad;                                                                      //  mouse radius
   float    kernel[400][400];                                                          //  radius <= 199

   editfunc    EFpaintransp;
}


//  menu function

void m_paint_transp(GtkWidget *, ch *menu)
{
   using namespace paint_transp_names;

   int   paint_transp_dialog_event(zdialog* zd, ch *event);
   void  paint_transp_mousefunc();

   ch       *mess1 = TX("left drag: add transparency \n"
                        "right drag: add opacity");

   F1_help_topic = TX("PAINT TRANSP");

   printf("m_paint_transp \n");

   EFpaintransp.menufunc = m_paint_transp;
   EFpaintransp.menuname = "Paint Transp";
   EFpaintransp.Farea = 2;                                                             //  select area usable
   EFpaintransp.mousefunc = paint_transp_mousefunc;                                    //  mouse function

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

   PXM_addalpha(E1pxm);                                                                //  add an alpha channel if req.
   PXM_addalpha(E3pxm);
   Fpaintnow();

   /***
             ________________________________
            |       Paint transparency       |
            |                                |
            |  left drag: add transparency   |
            |  right drag: add opacity       |
            |                                |
            |  paint radius   [____]         |
            |  power center   [____]         |
            |  power edge     [____]         |
            |  [x] gradual paint             |
            |                                |
            |                       [OK] [X] |
            |________________________________|

   ***/

   zdialog *zd = zdialog_new(TX("Paint Transparency"),Mwin,"OK","X",null);
   EFpaintransp.zd = zd;

   zdialog_add_widget(zd,"hbox","hbr","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labm","dialog",mess1,"space=5");
   zdialog_add_widget(zd,"hbox","hbbri","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbbr1","hbbri",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbbr2","hbbri",0,"homog|space=5");
   zdialog_add_widget(zd,"label","labbr","vbbr1",TX("paint radius"));
   zdialog_add_widget(zd,"label","labsc","vbbr1",TX("power center"));
   zdialog_add_widget(zd,"label","labse","vbbr1",TX("power edge"));
   zdialog_add_widget(zd,"zspin","radius","vbbr2","1|199|1|30");
   zdialog_add_widget(zd,"zspin","stcent","vbbr2","0|100|1|95");
   zdialog_add_widget(zd,"zspin","stedge","vbbr2","0|100|1|100");
   zdialog_add_widget(zd,"hbox","hb4","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","Fgrad","hb4",TX("gradual paint"),"space=5");

   zdialog_stuff(zd,"Fgrad",1);

   zdialog_load_inputs(zd);                                                            //  preload prior user inputs

   zdialog_run(zd,paint_transp_dialog_event,"save");                                   //  run dialog, parallel
   zdialog_send_event(zd,"radius");                                                    //  get kernel initialized

   zdialog_fetch(zd,"Fgrad",Fgrad);                                                    //  instant/gradual paint
   mode = 1;                                                                           //  start with paint mode

   takeMouse(paint_transp_mousefunc,drawcursor);                                       //  connect mouse function
   return;
}


//  dialog event and completion callback function

int paint_transp_dialog_event(zdialog *zd, ch *event)
{
   using namespace paint_transp_names;

   void  paint_transp_mousefunc();

   int         radius, dx, dy;
   float       rad, kern, stcent, stedge;

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

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

   if (strmatch(event,"focus"))                                                        //  toggle mouse capture
      takeMouse(paint_transp_mousefunc,drawcursor);

   if (zstrstr("radius stcent stedge",event))                                          //  get new brush attributes
   {
      zdialog_fetch(zd,"radius",Mrad);                                                 //  mouse radius
      zdialog_fetch(zd,"stcent",stcent);                                               //  center transparency
      zdialog_fetch(zd,"stedge",stedge);                                               //  edge transparency

      stcent = 0.01 * stcent;                                                          //  scale 0 ... 1
      stedge = 0.01 * stedge;

      radius = Mrad;

      for (dy = -radius; dy <= radius; dy++)                                           //  build kernel
      for (dx = -radius; dx <= radius; dx++)
      {
         rad = sqrt(dx*dx + dy*dy);
         kern = (radius - rad) / radius;                                               //  center ... edge  >>  1 ... 0
         kern = kern * (stcent - stedge) + stedge;                                     //  power  center ... edge
         if (kern < 0) kern = 0;
         if (kern > 1) kern = 1;
         if (rad > radius) kern = 2;                                                   //  beyond radius, within square
         kernel[dx+radius][dy+radius] = kern;
      }
   }

   if (strmatch(event,"Fgrad"))                                                        //  flag, gradual overpaints
      zdialog_fetch(zd,"Fgrad",Fgrad);

   return 1;
}


//  pixel paint mouse function

void paint_transp_mousefunc()
{
   using namespace paint_transp_names;

   void  paint_transp_dopixels(int px, int py);

   static int  pmxdown = 0, pmydown = 0;
   int         px, py;

   if (LMclick || RMclick)
   {
      if (LMclick) mode = 1;                                                           //  left click, paint
      if (RMclick) mode = 2;                                                           //  right click, erase

      px = Mxclick;
      py = Myclick;
      paint_transp_dopixels(px,py);                                                    //  do 1 block of pixels
   }

   else if (Mxdrag || Mydrag)                                                          //  drag in progress
   {
      if (Mbutton == 1) mode = 1;                                                      //  left drag, paint
      if (Mbutton == 3) mode = 2;                                                      //  right drag, erase

      px = Mxdrag;
      py = Mydrag;

      if (Mxdown != pmxdown || Mydown != pmydown) {                                    //  new drag
         pmxdown = Mxdown;
         pmydown = Mydown;
      }

      paint_transp_dopixels(px,py);                                                    //  do 1 block of pixels

      LMclick = RMclick = Mxdrag = Mydrag = 0;
      return;
   }

   LMclick = RMclick = Mxdrag = Mydrag = 0;
   draw_mousecircle(Mxposn,Myposn,Mrad,0,0);                                           //  draw mouse circle

   return;
}


//  paint or erase 1 block of pixels within mouse radius of px, py

void paint_transp_dopixels(int px, int py)
{
   using namespace paint_transp_names;

   float       *pix3;
   int         radius, dx, dy, qx, qy;
   int         ii, rr, dist = 0;
   float       kern;

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   radius = Mrad;

   for (dy = -radius; dy <= radius; dy++)                                              //  loop surrounding block of pixels
   for (dx = -radius; dx <= radius; dx++)
   {
      qx = px + dx;
      qy = py + dy;

      if (qx < 0 || qx > Eww-1) continue;
      if (qy < 0 || qy > Ehh-1) continue;

      if (sa_stat == sa_stat_fini) {                                                   //  select area active
         ii = qy * Eww + qx;
         dist = sa_pixmap[ii];
         if (! dist) continue;                                                         //  pixel is outside area
      }

      kern = kernel[dx+radius][dy+radius];                                             //  mouse transparencies
      if (kern > 1) continue;                                                          //  outside mouse radius

      pix3 = PXMpix(E3pxm,qx,qy);                                                      //  edited image pixel

      if (mode == 1) {                                                                 //  add transparency
         if (Fgrad) {
            pix3[3] -= 2 * kern;                                                       //  accumulate transparency
            if (pix3[3] < 0) pix3[3] = 0;
         }
         else pix3[3] = 0;                                                             //  instant
      }

      if (mode == 2) {                                                                 //  add opacity
         if (Fgrad) {
            pix3[3] += 2 * kern;                                                       //  accumulate
            if (pix3[3] > 255.0) pix3[3] = 255.0;
         }
         else pix3[3] = 255.0;                                                         //  instant
      }
   }

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

   px = px - radius - 1;                                                               //  repaint modified area
   py = py - radius - 1;
   rr = 2 * radius + 3;
   Fpaint3(px,py,rr,rr,cr);

   draw_mousecircle(Mxposn,Myposn,Mrad,0,cr);                                          //  draw mouse circle

   draw_context_destroy(draw_context);
   return;
}
/********************************************************************************/

//  fill selected areas or transparent areas with selected color

namespace areafill_names
{
   editfunc    EFareafill;
   int         Fareas, Ftransp;                                                        //  flags, fill select areas, fill transp. areas
   float       mblend;                                                                 //  0 - 1.0  input/output blend
   uint8       RGB[3] = { 0, 0, 0 };                                                   //  fill color
}


//  menu function

void m_area_fill(GtkWidget *, ch *menu)                                                //  25.50
{
   using namespace areafill_names;

   int    areafill_dialog_event(zdialog *zd, ch *event);
   void * areafill_thread(void *);

   ch    color[20];

   F1_help_topic = TX("AREA FILL");

   printf("m_area_fill \n");

   EFareafill.menuname = "Area Fill";
   EFareafill.menufunc = m_area_fill;
   EFareafill.threadfunc = areafill_thread;
   EFareafill.Farea = 2;                                                               //  select area usable
   EFareafill.Fscript = 1;                                                             //  scripting supported

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

   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

/***
          ________________________________
         |            Area Fill           |
         |                                |
         | color [####]  [palette]  [HSL] |
         |                                |
         | [_] fill selected areas        |
         | [_] fill transparent areas     |
         |                                |
         | Blend 0% ======[]======= 100%  |
         |                                |
         |                       [OK] [X] |
         |________________________________|

***/

   zdialog *zd = zdialog_new(TX("Area Fill"),Mwin,"OK","X",null);
   EFareafill.zd = zd;

   zdialog_add_widget(zd,"hbox","hbp","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labp","hbp",TX("color"),"space=5");
   zdialog_add_widget(zd,"colorbutt","colorbutt","hbp","0|0|0");
   zdialog_add_widget(zd,"button","palette","hbp",TX("palette"),"space=10");
   zdialog_add_widget(zd,"button","HSL","hbp","HSL");

   zdialog_add_widget(zd,"check","Fareas","dialog",TX("fill selected areas"));
   zdialog_add_widget(zd,"check","Ftransp","dialog",TX("fill transparent areas"));

   zdialog_add_widget(zd,"hbox","hbmblend","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab0","hbmblend",TX("Blend 0%"),"space=5");
   zdialog_add_widget(zd,"hscale","mblend","hbmblend","0.0|1.0|0.01|1.0","expand");
   zdialog_add_widget(zd,"label","lab100","hbmblend","100%","space=5");

   snprintf(color,20,"%d|%d|%d",RGB[0],RGB[1],RGB[2]);                                 //  initz. current color
   zdialog_stuff(zd,"colorbutt",color);

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

   return;
}


//  dialog event and completion callback function

int areafill_dialog_event(zdialog *zd, ch *event)
{
   using namespace areafill_names;

   int   err;
   ch    *pp, color[20];

   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)                                                              //  [OK]
      {
         edit_addhist("areas:%d transp:%d mblend:%.2f",                                //  edit params > edit hist
                                       Fareas,Ftransp,mblend);
         edit_done(0);                                                                 //  commit edit
      }

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

   if (strmatch(event,"focus")) return 1;

   if (strmatch(event,"colorbutt"))
   {
      zdialog_fetch(zd,"colorbutt",color,19);                                          //  get paint color from color button
      pp = substring(color,"|",1);
      if (pp) RGB[0] = atoi(pp);
      pp = substring(color,"|",2);
      if (pp) RGB[1] = atoi(pp);
      pp = substring(color,"|",3);
      if (pp) RGB[2] = atoi(pp);
   }

   if (strmatch(event,"palette"))
      err = RGB_chooser(zd,"colorbutt",RGB);                                           //  select color from palette

   if (strmatch(event,"HSL")) {
      err = HSL_chooser(zd,"colorbutt",RGB);                                           //  select color from palette
      if (err) return 1;
      snprintf(color,20,"%d|%d|%d",RGB[0],RGB[1],RGB[2]);
      zdialog_stuff(zd,"colorbutt",color);
   }

   if (strstr("Fareas Ftransp mblend",event))
   {
      zdialog_fetch(zd,"Fareas",Fareas);
      zdialog_fetch(zd,"Ftransp",Ftransp);
      zdialog_fetch(zd,"mblend",mblend);

      thread_signal();
   }

   return 1;
}


//  thread function

void * areafill_thread(void *)
{
   using namespace areafill_names;

   void * areafill_wthread(void *arg);

   if (! Fareas && ! Ftransp) return 0;                                                //  nothing to do

   do_wthreads(areafill_wthread,NSMP);                                                 //  worker threads

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

   Fpaint2();

   return 0;
}


//  worker thread functions

void * areafill_wthread(void *arg)
{
   using namespace areafill_names;

   int         index = *((int *) (arg));
   int         px, py;
   int         dist, Fdopixel;
   float       R1, G1, B1, R3, G3, B3;
   float       *pix1, *pix3;
   float       ff1, ff2;

   for (py = index; py < Ehh; py += NSMP)
   for (px = 0; px < Eww; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);                                                      //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                      //  output pixel

      Fdopixel = 0;

      if (Fareas && sa_stat == sa_stat_fini)                                           //  selected areas fill wanted
      {
         int ii = py * Eww + px;
         dist = sa_pixmap[ii];                                                         //  distance from edge
         if (dist) Fdopixel = 1;                                                       //  pixel inside area
      }

      if (Ftransp && pix1[3] < 250) Fdopixel = 1;                                      //  transparent areas fill wanted

      if (! Fdopixel) continue;

      R1 = pix1[0];                                                                    //  input pixel color
      G1 = pix1[1];
      B1 = pix1[2];

      R3 = RGB[0];                                                                     //  fill color
      G3 = RGB[1];
      B3 = RGB[2];

      if (pix1[3] < 250)                                                               //  pixel has transparency
      {
         ff1 = (255 - pix1[3]) / 255.0;                                                //  fill color part
         ff2 = 1.0 - ff1;                                                              //  input pixel part
         R3 = ff1 * R3 + ff2 * R1;                                                     //  blend input and fill color
         G3 = ff1 * G3 + ff2 * G1;
         B3 = ff1 * B3 + ff2 * B1;
      }

      if (mblend < 1.0)                                                                //  image blend slider
      {
         ff1 = mblend;
         ff2 = 1.0 - ff1;
         R3 = ff1 * R3 + ff2 * R1;                                                     //  blend input and fill color
         G3 = ff1 * G3 + ff2 * G1;
         B3 = ff1 * B3 + ff2 * B1;
      }

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

   return 0;
}


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

//  Copy in Image function.
//  Copy from one image area to another with variable opacity.

namespace copy_in_image
{
   int   dialog_event(zdialog* zd, ch *event);
   void  mousefunc();
   void  dopixels(int px, int py);                                                     //  update pixel block
   void  savepixB(int px, int py);                                                     //  save pixel block for poss. undo
   void  undolastB();                                                                  //  undo last pixel block, free memory
   void  freefirstB();                                                                 //  free memory for first pixel block
   void  freeallB();                                                                   //  free memory for all pixel blocks

   int      mode;                                                                      //  1/2 = paint / erase
   int      Mrad;                                                                      //  mouse radius
   int      xorg1, yorg1;                                                              //  source pixel origin
   int      xorg2, yorg2;                                                              //  dest pixel origin
   float    kernel[402][402];                                                          //  radius <= 200
   int      Fptran = 0;                                                                //  flag, paint over transparent areas
   int      nc, ac;                                                                    //  no. channels, alpha channel

   int64    maxmem = (int64) 4000 * MEGA;                                              //  max. pixel block memory
   int64    totmem;                                                                    //  pixB memory allocated
   int      maxpixB = 10000;                                                           //  max. pixel blocks
   int      totpixB = 0;                                                               //  total pixel blocks
   int      pixBseq = 0;                                                               //  last pixel block sequence no.

   typedef struct {                                                                    //  pixel block before edit
      int         seq;                                                                 //  block sequence no.
      uint16      px, py;                                                              //  center pixel (radius org.)
      uint16      radius;                                                              //  radius of pixel block
      float       pixel[][4];                                                          //  array of pixel[npix][4]
   }  pixBmem_t;

   pixBmem_t   **pixBmem = 0;                                                          //  *pixBmem_t[]

   int   pixBmem_cc = 12;                                                              //  all except pixel array + pad
   int   pcc4 = 4 * sizeof(float);                                                     //  pixel cc: RGBA = 4 channels

   editfunc    EFcopyinimage;
}


//  menu function

void m_copy_in_image(GtkWidget *, ch *menu)                                            //  separate paint and copy
{
   using namespace copy_in_image;

   ch       *mess1 = TX("left click: set image source location \n"
                        "right click: set corresponding destination");
   ch       *mess2 = TX("left drag: copy image to mouse position \n"
                        "right drag: restore original image");

   F1_help_topic = TX("COPY IN IMAGE");

   printf("m_copy_in_image \n");

   EFcopyinimage.menufunc = m_copy_in_image;
   EFcopyinimage.menuname = "Copy in Image";
   EFcopyinimage.Farea = 2;                                                            //  select area usable
   EFcopyinimage.mousefunc = mousefunc;                                                //  mouse function

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

   /***
             _______________________________________________
            |               Copy Within Image               |
            |                                               |
            |  left click: set image source location        |
            |  right click: set corresponding destination   |
            |                                               |
            |  left drag: copy image to mouse position      |
            |  right drag: restore original image           |
            |                                               |
            |  brush size      [____]     [undo last]       |
            |  opacity center  [____]     [undo all]        |
            |  opacity edge    [____]                       |
            |                                               |
            |  [x] paint transparent areas                  |
            |                                               |
            |                                      [OK] [X] |
            |_______________________________________________|

   ***/

   zdialog *zd = zdialog_new(TX("Copy Within Image"),Mwin,"OK","X",null);
   EFcopyinimage.zd = zd;

   zdialog_add_widget(zd,"hbox","hbm1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labm","hbm1",mess1,"space=3");

   zdialog_add_widget(zd,"hbox","hbm2","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labm","hbm2",mess2,"space=3");

   zdialog_add_widget(zd,"hsep","sep1","dialog",0,"space=5");

   zdialog_add_widget(zd,"hbox","hbbri","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbbr1","hbbri",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbbr2","hbbri",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","space","hbbri",0,"space=10");
   zdialog_add_widget(zd,"vbox","vbbr3","hbbri",0,"space=10");
   zdialog_add_widget(zd,"label","labbr","vbbr1",TX("brush size"));
   zdialog_add_widget(zd,"label","labtc","vbbr1",TX("opacity center"));
   zdialog_add_widget(zd,"label","labte","vbbr1",TX("opacity edge"));
   zdialog_add_widget(zd,"zspin","Mrad","vbbr2","1|200|1|30");
   zdialog_add_widget(zd,"zspin","opccent","vbbr2","1|100|1|30");
   zdialog_add_widget(zd,"zspin","opcedge","vbbr2","0|100|1|0");
   zdialog_add_widget(zd,"button","undlast","vbbr3",TX("Undo Last"));
   zdialog_add_widget(zd,"button","undall","vbbr3",TX("Undo All"));
   zdialog_add_widget(zd,"hbox","hb4","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","Fptran","hb4",TX("paint over transparent areas"),"space=5");

   zdialog_load_inputs(zd);                                                            //  preload prior user inputs
   zdialog_run(zd,dialog_event,"save");                                                //  run dialog, parallel

   zdialog_fetch(zd,"Fptran",Fptran);                                                  //  paint over transparent areas

   zdialog_send_event(zd,"Mrad");                                                      //  get kernel initialized

   totmem = 0;                                                                         //  memory used
   pixBmem = 0;                                                                        //  pixel block memory
   totpixB = 0;                                                                        //  pixel blocks
   pixBseq = 0;
   xorg1 = yorg1 = -1;                                                                 //  no source pixel origin
   xorg2 = yorg2 = -1;                                                                 //  no dest. pixel origin

   ac = 0;
   nc = E1pxm->nc;                                                                     //  channels, RGBA
   if (nc > 3) ac = 1;                                                                 //  alpha channel present

   takeMouse(mousefunc,drawcursor);                                                    //  connect mouse function
   return;
}


//  dialog event and completion callback function

int copy_in_image::dialog_event(zdialog *zd, ch *event)
{
   using namespace copy_in_image;

   int         radius, dx, dy;
   float       rad, kern, opccent, opcedge;

   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
      freeallB();                                                                      //  free pixel block memory
      return 1;
   }

   draw_mousecircle(0,0,0,1,0);                                                        //  erase mouse circle
   draw_mousecircle2(0,0,0,1,0);                                                       //  erase source tracking circle

   if (strmatch(event,"focus"))                                                        //  toggle mouse capture
      takeMouse(mousefunc,drawcursor);

   if (zstrstr("Mrad opccent opcedge",event))                                          //  get new brush attributes
   {
      zdialog_fetch(zd,"Mrad",Mrad);
      zdialog_fetch(zd,"opccent",opccent);
      zdialog_fetch(zd,"opcedge",opcedge);

      opccent = 0.01 * opccent;                                                        //  scale 0 ... 1
      opcedge = 0.01 * opcedge;
      opccent = pow(opccent,2);                                                        //  change response curve
      opcedge = opccent * opcedge;                                                     //  edge relative to center

      radius = Mrad;

      for (dy = -radius; dy <= radius; dy++)                                           //  build kernel
      for (dx = -radius; dx <= radius; dx++)
      {
         rad = sqrt(dx*dx + dy*dy);
         kern = (radius - rad) / radius;                                               //  center ... edge  >>  1 ... 0
         kern = kern * (opccent - opcedge) + opcedge;                                  //  opacity  center ... edge
         if (rad > radius) kern = 0;                                                   //  beyond radius, within square
         if (kern < 0) kern = 0;
         if (kern > 1) kern = 1;
         kernel[dx+radius][dy+radius] = kern;
      }
   }

   if (strmatch(event,"undlast"))                                                      //  undo last edit (click or drag)
      undolastB();

   if (strmatch(event,"undall")) {                                                     //  undo all edits
      edit_reset();
      freeallB();
   }

   if (strmatch(event,"Fptran"))                                                       //  flag, paint over transparency
      zdialog_fetch(zd,"Fptran",Fptran);

   return 1;
}


//  pixel paint mouse function

void copy_in_image::mousefunc()                                                        //  improve source/destination
{                                                                                      //    mouse tracking
   using namespace copy_in_image;

   static int  pmxdown = 0, pmydown = 0;
   int         px, py;

   if (LMclick) {
      xorg1 = Mxclick;                                                                 //  new source pixel origin
      yorg1 = Myclick;
      LMclick = 0;
   }

   if (RMclick) {                                                                      //  new corresp. destination pixel
      xorg2 = Mxclick;
      yorg2 = Myclick;
      RMclick = 0;
   }

   if ((Mxdrag || Mydrag) && xorg1 > 0 && xorg2 > 0)                                   //  drag in progress
   {
      if (Mbutton == 1) mode = 1;                                                      //  left drag, paint
      if (Mbutton == 3) mode = 2;                                                      //  right drag, erase

      if (Mxdown != pmxdown || Mydown != pmydown)                                      //  new drag
      {
         pmxdown = Mxdown;
         pmydown = Mydown;
         pixBseq++;                                                                    //  new undo seq. no.
      }

      px = Mxdrag;
      py = Mydrag;

      dopixels(px,py);                                                                 //  do 1 block of pixels
   }

   else mode = 0;

   draw_mousecircle(Mxposn,Myposn,Mrad,0,0);                                           //  draw mouse circle

   if (xorg1 > 0 && xorg2 > 0 && mode != 2) {                                          //  2nd circle tracks source pixels
      px = xorg1 + Mxposn - xorg2;
      py = yorg1 + Myposn - yorg2;
      if (px > 0 && px < Eww-1 && py > 0 && py < Ehh-1)
         draw_mousecircle2(px,py,Mrad,0,0);
   }
   else draw_mousecircle2(0,0,0,1,0);                                                  //  erase 2nd circle

   LMclick = RMclick = Mxdrag = Mydrag = 0;
   return;
}


//  paint or erase 1 block of pixels within mouse radius of px, py

void copy_in_image::dopixels(int px, int py)
{
   using namespace copy_in_image;

   float       *pix1, *pix3;
   int         radius, dx, dy, qx, qy, sx, sy;
   int         ii, ww, hh, dist = 0;
   int         pot = ac * Fptran;                                                      //  paint over transparent areas
   float       kern;

   if (xorg1 < 0 || yorg1 < 0) return;                                                 //  no source pixel origin
   if (xorg2 < 0 || yorg2 < 0) return;                                                 //  no dest. pixel origin

   cairo_t *cr = draw_context_create(gdkwin,draw_context);

   draw_mousecircle(0,0,0,1,cr);                                                       //  erase mouse circle
   draw_mousecircle2(0,0,0,1,cr);                                                      //  erase source tracking circle

   ww = Eww;
   hh = Ehh;

   savepixB(px,py);                                                                    //  save pixels for poss. undo

   radius = Mrad;

   if (mode == 1) {
      CEF->Fmods++;
      CEF->Fsaved = 0;
   }

   for (dy = -radius; dy <= radius; dy++)                                              //  loop surrounding block of pixels
   for (dx = -radius; dx <= radius; dx++)
   {
      qx = px + dx;
      qy = py + dy;

      if (qx < 0 || qx > ww-1) continue;
      if (qy < 0 || qy > hh-1) continue;

      if (sa_stat == sa_stat_fini) {                                                   //  select area active
         ii = qy * ww + qx;
         dist = sa_pixmap[ii];
         if (! dist) continue;                                                         //  pixel is outside area
      }

      kern = kernel[dx+radius][dy+radius];                                             //  mouse opacities
      if (kern == 0) continue;                                                         //  outside mouse radius

      pix1 = PXMpix(E1pxm,qx,qy);                                                      //  source image pixel
      pix3 = PXMpix(E3pxm,qx,qy);                                                      //  edited image pixel

      if (mode == 1)                                                                   //  paint
      {
         sx = xorg1 + qx - xorg2;                                                      //  source image pixel
         sy = yorg1 + qy - yorg2;
         if (sx < 0) sx = 0;
         if (sx > ww-1) sx = ww-1;
         if (sy < 0) sy = 0;
         if (sy > hh-1) sy = hh-1;
         pix1 = PXMpix(E1pxm,sx,sy);                                                   //  source image pixel at location

         kern = 0.3 * kern;
         pix3[0] = kern * pix1[0] + (1.0 - kern) * pix3[0];                            //  overpaints accumulate
         pix3[1] = kern * pix1[1] + (1.0 - kern) * pix3[1];
         pix3[2] = kern * pix1[2] + (1.0 - kern) * pix3[2];
         if (pot) pix3[3] = kern * pix1[3] + (1.0 - kern) * pix3[3];                   //  overpaint transparent area
      }

      if (mode == 2)                                                                   //  erase
      {
         pix3[0] = kern * pix1[0] + (1.0 - kern) * pix3[0];                            //  gradual erase
         pix3[1] = kern * pix1[1] + (1.0 - kern) * pix3[1];
         pix3[2] = kern * pix1[2] + (1.0 - kern) * pix3[2];
         if (pot) pix3[3] = kern * pix1[3] + (1.0 - kern) * pix3[3];
      }
   }

   px = px - radius - 1;                                                               //  repaint modified area
   py = py - radius - 1;
   ww = 2 * radius + 3;
   Fpaint3(px,py,ww,ww,cr);

   draw_context_destroy(draw_context);
   return;
}


//  save 1 block of pixels for possible undo

void copy_in_image::savepixB(int px, int py)
{
   using namespace copy_in_image;

   int            cc, npix, radius, dx, dy;
   float          *pix3;
   pixBmem_t      *save1B;

   if (! pixBmem) {                                                                    //  first time
      cc = maxpixB * sizeof(void *);
      pixBmem = (pixBmem_t **) zmalloc(cc,"copy in image");
      totpixB = 0;
      totmem = 0;
   }

   if (totmem > maxmem || totpixB == maxpixB)                                          //  free memory for oldest updates
      while (totmem > 0.7 * maxmem || totpixB > 0.7 * maxpixB)
         freefirstB();

   radius = Mrad;
   npix = 0;

   for (dy = -radius; dy <= radius; dy++)                                              //  count pixels in block
   for (dx = -radius; dx <= radius; dx++)
   {
      if (px + dx < 0 || px + dx > Eww-1) continue;
      if (py + dy < 0 || py + dy > Ehh-1) continue;
      npix++;
   }

   cc = npix * pcc4 + pixBmem_cc;
   save1B = (pixBmem_t *) zmalloc(cc,"copy in image");                                 //  allocate memory for block
   pixBmem[totpixB] = save1B;
   totpixB += 1;
   totmem += cc;

   save1B->seq = pixBseq;                                                              //  save pixel block poop
   save1B->px = px;
   save1B->py = py;
   save1B->radius = radius;

   npix = 0;

   for (dy = -radius; dy <= radius; dy++)                                              //  save pixels in block
   for (dx = -radius; dx <= radius; dx++)
   {
      if (px + dx < 0 || px + dx > Eww-1) continue;
      if (py + dy < 0 || py + dy > Ehh-1) continue;
      pix3 = PXMpix(E3pxm,(px+dx),(py+dy));                                            //  edited image pixel
      save1B->pixel[npix][0] = pix3[0];
      save1B->pixel[npix][1] = pix3[1];
      save1B->pixel[npix][2] = pix3[2];
      if (ac) save1B->pixel[npix][3] = pix3[3];
      npix++;
   }

   return;
}


//  undo last pixel block (newest edit) and free memory

void copy_in_image::undolastB()
{
   using namespace copy_in_image;

   int            ii, cc, npix, radius;
   int            ww, px, py, dx, dy;
   float          *pix3;
   pixBmem_t      *save1B;

   for (ii = totpixB-1; ii >= 0; ii--)
   {
      save1B = pixBmem[ii];
      if (save1B->seq != pixBseq) break;
      px = save1B->px;
      py = save1B->py;
      radius = save1B->radius;

      npix = 0;
      for (dy = -radius; dy <= radius; dy++)
      for (dx = -radius; dx <= radius; dx++)
      {
         if (px + dx < 0 || px + dx > Eww-1) continue;
         if (py + dy < 0 || py + dy > Ehh-1) continue;
         pix3 = PXMpix(E3pxm,(px+dx),(py+dy));
         pix3[0] = save1B->pixel[npix][0];
         pix3[1] = save1B->pixel[npix][1];
         pix3[2] = save1B->pixel[npix][2];
         if (ac) pix3[3] = save1B->pixel[npix][3];
         npix++;
      }

      px = px - radius - 1;
      py = py - radius - 1;
      ww = 2 * radius + 3;
      Fpaint3(px,py,ww,ww,0);

      zfree(save1B);
      pixBmem[ii] = 0;
      cc = npix * pcc4 + pixBmem_cc;
      totmem -= cc;
      totpixB--;
   }

   if (pixBseq > 0) --pixBseq;
   return;
}


//  free memory for first pixel block (oldest edit)

void copy_in_image::freefirstB()
{
   using namespace copy_in_image;

   int            firstseq;
   int            ii, jj, cc, npix, radius;
   int            px, py, dx, dy;
   pixBmem_t      *save1B;

   if (! totpixB) return;
   firstseq = pixBmem[0]->seq;

   for (ii = 0; ii < totpixB; ii++)
   {
      save1B = pixBmem[ii];
      if (save1B->seq != firstseq) break;
      px = save1B->px;
      py = save1B->py;
      radius = save1B->radius;
      npix = 0;
      for (dy = -radius; dy <= radius; dy++)
      for (dx = -radius; dx <= radius; dx++)
      {
         if (px + dx < 0 || px + dx > Eww-1) continue;
         if (py + dy < 0 || py + dy > Ehh-1) continue;
         npix++;
      }

      zfree(save1B);
      pixBmem[ii] = 0;
      cc = npix * pcc4 + pixBmem_cc;
      totmem -= cc;
   }

   for (jj = 0; ii < totpixB; jj++, ii++)
      pixBmem[jj] = pixBmem[ii];

   totpixB = jj;
   return;
}


//  free all pixel block memory

void copy_in_image::freeallB()
{
   using namespace copy_in_image;

   int            ii;
   pixBmem_t      *save1B;

   for (ii = totpixB-1; ii >= 0; ii--)
   {
      save1B = pixBmem[ii];
      zfree(save1B);
   }

   if (pixBmem) zfree(pixBmem);
   pixBmem = 0;

   pixBseq = 0;
   totpixB = 0;
   totmem = 0;

   return;
}


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

//  Copy From Image function.
//  'Paint' a target image with pixels from a different source image.

namespace copy_from_image
{
   int   dialog_event(zdialog* zd, ch *event);
   void  mousefunc();                                                                  //  generate mouse circle
   void  dopixels();                                                                   //  copy pixel block to target
   void  save_pixblock();                                                              //  save pixel block for poss. undo
   void  undo_lastblock();                                                             //  undo last pixel block, free memory
   void  free_firstblock();                                                            //  free memory for first pixel block
   void  free_allblocks();                                                             //  free memory for all pixel blocks

   editfunc    EFcopyfromimage;

   int      mode;                                                                      //  1/2 = paint / erase
   int      Fptran;                                                                    //  flag, paint over transparent areas

   int      mpxC, mpyC;                                                                //  center of pixel copy area
   int      mpx, mpy;                                                                  //  center of moving mouse circle
   int      mrad;                                                                      //  radius of pixel copy area

   typedef struct {                                                                    //  shared memory data
      int      killsource;                                                             //  source image process should exit
      int      mpxC, mpyC;                                                             //  center of pixel copy area (dest image)
      int      mpxS, mpyS;                                                             //  center of pixel copy area (source image)
      int      mpx, mpy;                                                               //  mouse drag position
      int      mrad;                                                                   //  mouse radius
      float    Fscale;                                                                 //  source image scale factor
      int      Freq;                                                                   //  source/target coord. flag
      float    pixels[402*402*4];                                                      //  pixel block, max. mrad = 200
   }  mmap_data_t;

   mmap_data_t    *mmap_data;                                                          //  shared memory pointer

   float    kernel[402][402];                                                          //  mrad <= 200

   int64    maxmem = (int64) 4000 * MEGA;                                              //  max. pixel block memory
   int64    totmem;                                                                    //  pixblock memory allocated
   int      maxpixblock = 10000;                                                       //  max. pixel blocks
   int      totpixblock = 0;                                                           //  total pixel blocks
   int      pixblockseq = 0;                                                           //  last pixel block sequence no.

   typedef struct {                                                                    //  saved pixel block before edit
      int         seq;                                                                 //  block sequence no.
      uint16      mpx, mpy;                                                            //  center pixel (mrad org.)
      uint16      mrad;                                                                //  radius of pixel block
      float       pixels[][4];                                                         //  array of pixels, rows x cols
   }  pixblockmem_t;

   pixblockmem_t   **pixblockmem;
   int      pixblockmem_cc = 12;                                                       //  all except pixel array + pad
}


//  menu function

void m_copy_from_image(GtkWidget *, ch *menu)                                          //  split source/target processes
{
   using namespace copy_from_image;

   int      err, fd;
   size_t   cc;

   F1_help_topic = TX("COPY FROM IMAGE");

   printf("m_copy_from_image \n");

   fd = shm_open("/fotocx_copy_from_image",O_RDWR+O_CREAT,0600);                       //  identify memory mapped region
   if (fd < 0) {
      zmessageACK(Mwin,"shm_open() failure: %s",strerror(errno));
      return;
   }

   cc = sizeof(mmap_data_t);
   err = ftruncate(fd,cc);
   if (err) {
      zmessageACK(Mwin,"ftruncate() failure: %s",strerror(errno));
      return;
   }

   mmap_data = (mmap_data_t *) mmap(0,cc,PROT_WRITE,MAP_SHARED,fd,0);                  //  create memory mapped region
   if (mmap_data == (void *) -1) {
      zmessageACK(Mwin,"mmap() failure: %s",strerror(errno));
      return;
   }

   memset(mmap_data,0,cc);

   mpxC = mpyC = 0;                                                                    //  default pixel copy area selected
   mrad = 50;                                                                          //  default mouse radius
   mmap_data->Fscale = 1.0;                                                            //  default source image scale factor

   EFcopyfromimage.menufunc = m_copy_from_image;                                       //  start edit function for target image
   EFcopyfromimage.menuname = "Copy From Image";
   EFcopyfromimage.Farea = 2;                                                          //  select area usable
   EFcopyfromimage.mousefunc = mousefunc;                                              //  mouse function

   if (! edit_setup(EFcopyfromimage)) return;

   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

   cc = maxpixblock * sizeof(void *);
   pixblockmem = (pixblockmem_t **) zmalloc(cc,"copy from image");                     //  saved pixel blocks list
   totmem = 0;                                                                         //  memory used
   totpixblock = 0;                                                                    //  pixel blocks
   pixblockseq = 0;

   /****
             __________________________________________________
            |                 Copy From Image                  |
            |                                                  |
            |  shift + left click: synchronize copy position   |
            |  left drag: copy source image to mouse           |
            |  right drag: restore original image              |
            |                                                  |
            |  source image scale [____]                       |
            |                                                  |
            |  brush size      [____]     [undo last]          |
            |  opacity center  [____]     [undo all]           |
            |  opacity edge    [____]                          |
            |                                                  |
            |  [x] paint over transparent areas                |
            |                                                  |
            |                                         [OK] [X] |
            |__________________________________________________|

   ****/

   ch       *mess1 = TX("shift + left click: synchronize copy position \n"
                        "left drag: copy source image to mouse \n"
                        "right drag: restore original image");

   zdialog *zd = zdialog_new(TX("Copy From Image"),Mwin,"OK","X",null);
   EFcopyfromimage.zd = zd;

   zdialog_add_widget(zd,"hbox","hbr","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labm","dialog",mess1,"space=5");
   zdialog_add_widget(zd,"hbox","hbsc","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labsc","hbsc",TX("source image scale"),"space=3");
   zdialog_add_widget(zd,"zspin","scale","hbsc","0.2|5.0|0.01|1.0","space=3");
   zdialog_add_widget(zd,"hbox","hbbri","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbbr1","hbbri",0,"homog|space=3");
   zdialog_add_widget(zd,"vbox","vbbr2","hbbri",0,"homog|space=3");
   zdialog_add_widget(zd,"vbox","space","hbbri",0,"space=10");
   zdialog_add_widget(zd,"vbox","vbbr3","hbbri",0,"space=10");
   zdialog_add_widget(zd,"label","labbr","vbbr1",TX("brush size"));
   zdialog_add_widget(zd,"label","labtc","vbbr1",TX("opacity center"));
   zdialog_add_widget(zd,"label","labte","vbbr1",TX("opacity edge"));
   zdialog_add_widget(zd,"zspin","mrad","vbbr2","1|200|1|50");
   zdialog_add_widget(zd,"zspin","opccent","vbbr2","1|100|1|10");
   zdialog_add_widget(zd,"zspin","opcedge","vbbr2","0|100|1|0");
   zdialog_add_widget(zd,"button","undlast","vbbr3",TX("Undo Last"));
   zdialog_add_widget(zd,"button","undall","vbbr3",TX("Undo All"));
   zdialog_add_widget(zd,"hbox","hb4","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","Fptran","hb4",TX("paint over transparent areas"),"space=5");

   zdialog_load_inputs(zd);                                                            //  preload prior user inputs
   zdialog_run(zd,dialog_event,"save");                                                //  run dialog, parallel

   zdialog_fetch(zd,"Fptran",Fptran);                                                  //  paint over transparent areas
   zdialog_fetch(zd,"scale",mmap_data->Fscale);

   zdialog_send_event(zd,"mrad");                                                      //  get kernel initialized

   takeMouse(mousefunc,drawcursor);                                                    //  connect mouse function

   save_params();                                                                      //  communicate current file
   new_session("-x1 -m \"Copy From Image Slave\" ");                                   //  slave process for source image

   return;
}


//  dialog event and completion callback function

int copy_from_image::dialog_event(zdialog *zd, ch *event)
{
   using namespace copy_from_image;

   int         dx, dy;
   float       rad, kern, opccent, opcedge;

   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
      free_allblocks();                                                                //  free pixel block memory
      zfree(pixblockmem);
      mmap_data->killsource = 1;                                                       //  make source image process exit
      return 1;
   }

   if (strmatch(event,"focus"))                                                        //  toggle mouse capture
      takeMouse(mousefunc,drawcursor);

   if (strmatch(event,"scale"))                                                        //  source image scale change
      zdialog_fetch(zd,"scale",mmap_data->Fscale);

   if (zstrstr("mrad opccent opcedge",event))                                          //  get new brush attributes
   {
      zdialog_fetch(zd,"mrad",mrad);
      zdialog_fetch(zd,"opccent",opccent);
      zdialog_fetch(zd,"opcedge",opcedge);

      opccent = 0.01 * opccent;                                                        //  scale 0 ... 1
      opcedge = 0.01 * opcedge;
      opccent = pow(opccent,2);                                                        //  change response curve
      opcedge = opccent * opcedge;                                                     //  edge relative to center

      for (dy = -mrad; dy <= mrad; dy++)                                               //  rebuild kernel
      for (dx = -mrad; dx <= mrad; dx++)
      {
         rad = sqrt(dx*dx + dy*dy);
         kern = (mrad - rad) / mrad;                                                   //  center ... edge  >>  1 ... 0
         kern = kern * (opccent - opcedge) + opcedge;                                  //  opacity  center ... edge
         if (rad > mrad) kern = 0;                                                     //  beyond mrad, within square
         if (kern < 0) kern = 0;
         if (kern > 1) kern = 1;
         kernel[dx+mrad][dy+mrad] = kern;
      }
   }

   if (strmatch(event,"undlast"))                                                      //  undo last edit (click or drag)
      undo_lastblock();

   if (strmatch(event,"undall")) {                                                     //  undo all edits
      edit_reset();
      free_allblocks();
   }

   if (strmatch(event,"Fptran"))                                                       //  flag, paint over transparency
      zdialog_fetch(zd,"Fptran",Fptran);

   return 1;
}


//  mouse function

void copy_from_image::mousefunc()
{
   using namespace copy_from_image;

   static int  pmxdown = 0, pmydown = 0;
   static int  pmpx, pmpy;

   mode = 0;

   mpx = Mxposn;
   mpy = Myposn;

   mmap_data->mpx = mpx;                                                               //  inform source image,
   mmap_data->mpy = mpy;                                                               //    mouse position,
   mmap_data->mrad = mrad;                                                             //    mouse radius

   if (LMclick && KBshiftkey)                                                          //  shift + left mouse click
   {
      Mxdown = Mxclick;                                                                //  click position
      Mydown = Myclick;
      mmap_data->mpxC = Mxdown;                                                        //  inform source image,
      mmap_data->mpyC = Mydown;                                                        //    new mouse drag start
      pixblockseq++;                                                                   //  new undo seq. no.
      mode = 1;

      if (! mmap_data->Freq) {                                                         //  request pixel block from source
         mmap_data->Freq = 1;
         pmpx = mpx;                                                                   //  save mouse position
         pmpy = mpy;
      }
   }

   else if (Mxdrag || Mydrag)                                                          //  drag in progress
   {
      if (Mbutton == 1) mode = 1;                                                      //  left drag, paint
      if (Mbutton == 3) mode = 2;                                                      //  right drag, erase

      if (Mxdown != pmxdown || Mydown != pmydown) {                                    //  new drag
         pmxdown = Mxdown;
         pmydown = Mydown;
         if (mode == 1) pixblockseq++;                                                 //  new undo seq. no.
      }

      if (mode == 1 && ! mmap_data->Freq) {                                            //  request pixel block from source
         mmap_data->Freq = 1;
         pmpx = mpx;                                                                   //  save mouse position
         pmpy = mpy;
      }
   }

   if (mode == 1 && mmap_data->Freq == 2) {                                            //  new pixel block from source avail.
      mpx = pmpx;                                                                      //  synch mouse position
      mpy = pmpy;
      dopixels();                                                                      //  paint pixel block on target image
      mmap_data->Freq = 0;                                                             //  ready for next
   }

   if (mode == 2) dopixels();                                                          //  erase target image

   draw_mousecircle(0,0,0,1,0);                                                        //  erase mouse circle
   draw_mousecircle(mpx,mpy,mrad,0,0);                                                 //  draw mouse circle

   LMclick = RMclick = Mxdrag = Mydrag = 0;
   return;
}


//  paint or erase 1 block of pixels within mouse radius of mpx, mpy

void copy_from_image::dopixels()
{
   using namespace copy_from_image;

   float       *pix1, *pix2, *pix3, pixF[4];
   int         dx, dy, qx, qy, px, py;
   int         ii, ww, hh, rs, dist = 0;
   int         pot = Fptran;                                                           //  paint over transparent areas
   float       kern, alpha1, alpha2;

   ww = Eww;
   hh = Ehh;
   rs = 2 * mrad + 1;                                                                  //  pixel block row stride

   save_pixblock();                                                                    //  save pixels for poss. undo

   for (dy = -mrad; dy <= mrad; dy++)                                                  //  loop surrounding block of pixels
   for (dx = -mrad; dx <= mrad; dx++)
   {
      qx = mpx + dx;
      qy = mpy + dy;

      if (qx < 0 || qx > ww-1) continue;
      if (qy < 0 || qy > hh-1) continue;

      if (sa_stat == sa_stat_fini) {                                                   //  select area active
         ii = qy * ww + qx;
         dist = sa_pixmap[ii];
         if (! dist) continue;                                                         //  pixel is outside area
      }

      kern = kernel[dx+mrad][dy+mrad];                                                 //  mouse opacities
      if (kern == 0) continue;                                                         //  outside mouse radius

      pix1 = PXMpix(E1pxm,qx,qy);                                                      //  input target image pixel
      ii = (dy + mrad) * rs + (dx + mrad);                                             //  source image pixel >> target
      pix2 = mmap_data->pixels + ii * 4;
      pix3 = PXMpix(E3pxm,qx,qy);                                                      //  output target image pixel

      if (mode == 1)                                                                   //  paint
      {
         if (pot) alpha1 = 1;                                                          //  paint over transparent areas
         else alpha1 = 0.00392 * pix1[3];                                              //  input target image opacity
         alpha2 = 0.00392 * pix2[3];                                                   //  source image opacity

         if (alpha2 < 0.99) {
            pixF[0] = pix2[0] * alpha2 + pix1[0] * (1 - alpha2);                       //  construct output target pixel
            pixF[1] = pix2[1] * alpha2 + pix1[1] * (1 - alpha2);                       //   = source image pixel
            pixF[2] = pix2[2] * alpha2 + pix1[2] * (1 - alpha2);                       //   + target image pixel
            pixF[3] = pix2[3] * alpha2 + pix1[3] * (1 - alpha2);
         }
         else {
            pixF[0] = pix2[0];                                                         //  (no transparency case)
            pixF[1] = pix2[1];
            pixF[2] = pix2[2];
            pixF[3] = pix2[3];
         }

         if (alpha1 < 0.99) {
            pixF[0] = pixF[0] * alpha1;                                                //  apply target image opacity
            pixF[1] = pixF[1] * alpha1;
            pixF[2] = pixF[2] * alpha1;
            pixF[3] = pixF[3] * alpha1;
         }

         kern = 0.2 * kern;                                                            //  output target image changes
         pix3[0] = kern * pixF[0] + (1 - kern) * pix3[0];                              //    gradually to source image
         pix3[1] = kern * pixF[1] + (1 - kern) * pix3[1];
         pix3[2] = kern * pixF[2] + (1 - kern) * pix3[2];
         pix3[3] = kern * pixF[3] + (1 - kern) * pix3[3];
      }

      if (mode == 2)                                                                   //  erase
      {
         kern = 0.4 * kern;
         pix3[0] = kern * pix1[0] + (1.0 - kern) * pix3[0];                            //  gradual erase
         pix3[1] = kern * pix1[1] + (1.0 - kern) * pix3[1];
         pix3[2] = kern * pix1[2] + (1.0 - kern) * pix3[2];
         pix3[3] = kern * pix1[3] + (1.0 - kern) * pix3[3];
      }
   }

   if (mode == 1) {
      CEF->Fmods++;
      CEF->Fsaved = 0;
   }

   px = mpx - mrad - 1;                                                                //  repaint modified area
   py = mpy - mrad - 1;
   ww = 2 * mrad + 3;
   Fpaint3(px,py,ww,ww,0);

   return;
}


//  save 1 block of pixels for possible undo

void copy_from_image::save_pixblock()
{
   using namespace copy_from_image;

   int            cc, npix, dx, dy;
   int            pcc = 4 * sizeof(float);
   float          *pix3;
   pixblockmem_t  *save1B;

   if (totmem > maxmem || totpixblock == maxpixblock)                                  //  free memory for oldest updates
      while (totmem > 0.7 * maxmem || totpixblock > 0.7 * maxpixblock)
         free_firstblock();

   npix = 0;
   for (dy = -mrad; dy <= mrad; dy++)                                                  //  count pixels in block
   for (dx = -mrad; dx <= mrad; dx++)
   {
      if (mpx + dx < 0 || mpx + dx > Eww-1) continue;
      if (mpy + dy < 0 || mpy + dy > Ehh-1) continue;
      npix++;
   }

   cc = npix * pcc + pixblockmem_cc;
   save1B = (pixblockmem_t *) zmalloc(cc,"copy from image");                           //  allocate memory for block
   pixblockmem[totpixblock] = save1B;
   totpixblock += 1;
   totmem += cc;

   save1B->seq = pixblockseq;                                                          //  save pixel block poop
   save1B->mpx = mpx;
   save1B->mpy = mpy;
   save1B->mrad = mrad;

   npix = 0;
   for (dy = -mrad; dy <= mrad; dy++)                                                  //  save pixels in block
   for (dx = -mrad; dx <= mrad; dx++)
   {
      if (mpx + dx < 0 || mpx + dx > Eww-1) continue;
      if (mpy + dy < 0 || mpy + dy > Ehh-1) continue;
      pix3 = PXMpix(E3pxm,(mpx+dx),(mpy+dy));                                          //  edited image pixel
      memcpy(save1B->pixels[npix],pix3,pcc);
      npix++;
   }

   return;
}


//  undo last pixel block (newest edit) and free memory

void copy_from_image::undo_lastblock()
{
   using namespace copy_from_image;

   int            ii, cc, npix;
   int            ww, px, py, dx, dy;
   int            pcc = 4 * sizeof(float);
   float          *pix3;
   pixblockmem_t  *save1B;

   for (ii = totpixblock-1; ii >= 0; ii--)
   {
      save1B = pixblockmem[ii];
      if (save1B->seq != pixblockseq) break;
      px = save1B->mpx;
      py = save1B->mpy;
      mrad = save1B->mrad;

      npix = 0;
      for (dy = -mrad; dy <= mrad; dy++)
      for (dx = -mrad; dx <= mrad; dx++)
      {
         if (px + dx < 0 || px + dx > Eww-1) continue;
         if (py + dy < 0 || py + dy > Ehh-1) continue;
         pix3 = PXMpix(E3pxm,(px+dx),(py+dy));
         memcpy(pix3,save1B->pixels[npix],pcc);
         npix++;
      }

      px = px - mrad - 1;
      py = py - mrad - 1;
      ww = 2 * mrad + 3;
      Fpaint3(px,py,ww,ww,0);

      zfree(save1B);
      pixblockmem[ii] = 0;
      cc = npix * pcc + pixblockmem_cc;
      totmem -= cc;
      totpixblock--;
   }

   if (pixblockseq > 0) --pixblockseq;
   return;
}


//  free memory for first pixel block (oldest edit)

void copy_from_image::free_firstblock()
{
   using namespace copy_from_image;

   int            firstseq;
   int            ii, jj, cc, npix;
   int            px, py, dx, dy;
   int            pcc = 4 * sizeof(float);
   pixblockmem_t  *save1B;

   if (! totpixblock) return;
   firstseq = pixblockmem[0]->seq;

   for (ii = 0; ii < totpixblock; ii++)
   {
      save1B = pixblockmem[ii];
      if (save1B->seq != firstseq) break;
      px = save1B->mpx;
      py = save1B->mpy;
      mrad = save1B->mrad;

      npix = 0;
      for (dy = -mrad; dy <= mrad; dy++)
      for (dx = -mrad; dx <= mrad; dx++)
      {
         if (px + dx < 0 || px + dx > Eww-1) continue;
         if (py + dy < 0 || py + dy > Ehh-1) continue;
         npix++;
      }

      zfree(save1B);
      pixblockmem[ii] = 0;
      cc = npix * pcc + pixblockmem_cc;
      totmem -= cc;
   }

   for (jj = 0; ii < totpixblock; jj++, ii++)
      pixblockmem[jj] = pixblockmem[ii];

   totpixblock = jj;
   return;
}


//  free all pixel block memory

void copy_from_image::free_allblocks()
{
   using namespace copy_from_image;

   int            ii;
   pixblockmem_t  *save1B;

   for (ii = totpixblock-1; ii >= 0; ii--)
   {
      save1B = pixblockmem[ii];
      zfree(save1B);
   }

   pixblockseq = 0;
   totpixblock = 0;
   totmem = 0;

   return;
}


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

//  Copy From Image slave process - copy pixels from source image into shared memory.
//  Started by m_copy_from_image() as a separate session to view the source image.

namespace copy_from_image_slave
{
   void  mousefunc();
   void  dopixels();                                                                   //  update pixel block

   PXM      *pxm_1x = 0, *pxm_scaled = 0;
   int      mpxC, mpyC;                                                                //  center of pixel copy area
   int      mpx, mpy;                                                                  //  center of moving mouse circle
   int      mrad;                                                                      //  radius of pixel copy area
   float    Fscale = 1;

   typedef struct {                                                                    //  shared memory data
      int      killsource;                                                             //  source image process should exit
      int      mpxC, mpyC;                                                             //  center of pixel copy area (dest image)
      int      mpxS, mpyS;                                                             //  center of pixel copy area (source image)
      int      mpx, mpy;                                                               //  mouse drag position
      int      mrad;                                                                   //  mouse radius
      float    Fscale;                                                                 //  source image scale factor
      int      Freq;                                                                   //  source/target coord. flag
      float    pixels[402*402*4];                                                      //  pixel block, max. mrad = 200
   }  mmap_data_t;

   mmap_data_t    *mmap_data;                                                          //  shared memory pointer
}


//  menu function

void m_copy_from_image_slave(GtkWidget *, ch *)                                        //  split source/target processes
{
   using namespace copy_from_image_slave;

   int      err, fd;
   int      pmpx = 0, pmpy = 0, pmrad = 0;
   size_t   cc;

   F1_help_topic = TX("COPY FROM IMAGE");

   printf("m_copy_from_image_slave \n");

   fd = shm_open("/fotocx_copy_from_image",O_RDWR+O_CREAT,0600);                       //  identify memory mapped region
   if (fd < 0) {
      zmessageACK(Mwin,"shm_open() failure: %s",strerror(errno));
      return;
   }

   cc = sizeof(mmap_data_t);
   err = ftruncate(fd,cc);
   if (err) {
      zmessageACK(Mwin,TX("ftruncate() failure: %s"),strerror(errno));
      return;
   }

   mmap_data = (mmap_data_t *) mmap(0,cc,PROT_WRITE,MAP_SHARED,fd,0);                  //  create memory mapped region
   if (mmap_data == (void *) -1) {
      zmessageACK(Mwin,TX("mmap() failure: %s"),strerror(errno));
      return;
   }

   ch       *Pcurr_file = 0;
   int      Frefresh = 1;
   int      ww, hh;
   int      mpx2, mpy2, mrad2;

   mrad = 50;

   while (true)
   {
      zmainloop();

      if (! curr_file) {
         zmainsleep(1);
         continue;
      }

      if (mmap_data->killsource) goto done;                                            //  target image process is done

      if (! Pcurr_file || ! strmatch(curr_file,Pcurr_file)) {                          //  detect file change
         zstrcopy(Pcurr_file,curr_file,"copy from image");
         Frefresh = 1;
      }

      if (CEF) {                                                                       //  source image edit, wait until done
         Frefresh = 1;
         continue;
      }

      if (mmap_data->Fscale != Fscale) Frefresh = 1;

      if (Frefresh)
      {
         printf("m_copy_from_image_slave() source image: %s \n",curr_file);
         if (pxm_1x) PXM_free(pxm_1x);                                                 //  refresh edited image
         if (E0pxm) pxm_1x = PXM_copy(E0pxm);
         else pxm_1x = PXM_load(curr_file,1);                                          //  load new image
         if (! pxm_1x) goto fail;
         Fscale = mmap_data->Fscale;
         ww = pxm_1x->ww * Fscale;
         hh = pxm_1x->hh * Fscale;
         if (pxm_scaled) PXM_free(pxm_scaled);
         pxm_scaled = PXM_rescale(pxm_1x,ww,hh);
         if (! pxm_scaled) goto fail;
         PXM_addalpha(pxm_scaled);
         takeMouse(mousefunc,targcursor);                                              //  connect mouse function
         Frefresh = 0;
      }

      mpx = mpxC + mmap_data->mpx - mmap_data->mpxC;                                   //  convert target copy area
      mpy = mpyC + mmap_data->mpy - mmap_data->mpyC;                                   //    to source copy area
      mrad = mmap_data->mrad;

      if (mpx != pmpx || mpy != pmpy || mrad != pmrad) {                               //  mouse circle changed
         pmpx = mpx;
         pmpy = mpy;
         pmrad = mrad;
         mpx2 = mpx / Fscale;
         mpy2 = mpy / Fscale;
         mrad2 = mrad / Fscale;
         gdk_window_freeze_updates(gdkwin);
         draw_mousecircle(0,0,0,1,0);                                                  //  erase mouse circle
         draw_mousecircle(mpx2,mpy2,mrad2,0,0);                                        //  draw mouse circle
         gdk_window_thaw_updates(gdkwin);
         zmainloop();
      }

      if (mmap_data->Freq == 1) {                                                      //  pixels wanted from target image
         dopixels();                                                                   //  copy pixel block to shared memory
         mmap_data->Freq = 2;                                                          //  pixel block available
      }
   }

fail:
   zmessageACK(Mwin,TX("source image failure (scale too big?)"));

done:
   if (CEF) edit_cancel(0);
   printf("m_copy_from_image_slave() exit \n");
   quitxx();
}


//  mouse function

void copy_from_image_slave::mousefunc()
{
   using namespace copy_from_image_slave;

   if (LMclick && KBshiftkey) {                                                        //  shift + left mouse click
      LMclick = 0;
      mmap_data->mpxS = Mxclick;                                                       //  source image mouse center
      mmap_data->mpyS = Myclick;
      mpxC = Mxclick * Fscale;                                                         //  source pixel copy area
      mpyC = Myclick * Fscale;
   }

   return;
}


//  copy block of pixels within mouse circle to shared memory

void copy_from_image_slave::dopixels()
{
   using namespace copy_from_image_slave;

   float       *pix0, *pix1;
   float       pixnull[4] = { 0, 0, 0, 0 };                                            //  black, alpha = 0
   int         dx, dy, qx, qy;
   int         ii, ww, hh, rs;
   int         pcc = 4 * sizeof(float);

   ww = pxm_scaled->ww;
   hh = pxm_scaled->hh;

   rs = 2 * mrad + 1;                                                                  //  pixel block row stride

   for (dy = -mrad; dy <= mrad; dy++)                                                  //  loop surrounding block of pixels
   for (dx = -mrad; dx <= mrad; dx++)
   {
      qx = mpx + dx;
      qy = mpy + dy;

      if (qx < 0 || qx > ww-1 || qy < 0 || qy > hh-1) pix0 = pixnull;                  //  pixel outside source image
      else pix0 = PXMpix(pxm_scaled,qx,qy);                                            //  source image pixel
      ii = (dy + mrad) * rs + (dx + mrad);
      pix1 = mmap_data->pixels + ii * 4;                                               //  to shared memory
      memcpy(pix1,pix0,pcc);
   }

   return;
}


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

//  Copy pixels from prior edit steps of current image.
//  Mouse can be weak or strong, and edits are removed incrementally.
//  E3 image (target) is incrementally replaced from chosen source image
//  (one of the previous edit steps for the current image).

namespace copy_prior_edit_names
{
   int      radius;
   int      cpower;
   int      epower;
   editfunc EFcopy_prior_edit;
}


//  menu function

void m_copy_prior_edit(GtkWidget *, ch *menu)                                          //  menu function
{
   using namespace copy_prior_edit_names;

   int   copy_prior_edit_dialog_event(zdialog *, ch *event);                           //  dialog event function
   void  copy_prior_edit_mousefunc();                                                  //  mouse function

   zdialog  *zd;
   ch       text[40];

   F1_help_topic = TX("COPY PRIOR EDIT");

   printf("m_copy_prior_edit \n");

   if (CEF) {
      zmessageACK(Mwin,TX("finish current edit first"));
      return;
   }

   if (URS_pos < 1) {
      zmessageACK(Mwin,TX("no prior edit"));
      return;
   }

   if (! E0pxm) {
      zmessageACK(Mwin,TX("no current edit image"));
      return;
   }

   EFcopy_prior_edit.menufunc = m_copy_prior_edit;                                     //  start new edit
   EFcopy_prior_edit.menuname = "Copy Prior Edit";
   EFcopy_prior_edit.Farea = 1;                                                        //  ignore area

   EFcopy_prior_edit.mousefunc = copy_prior_edit_mousefunc;

   if (! edit_setup(EFcopy_prior_edit)) return;                                        //  initz. edit

/***
          ______________________________________
         |        Copy Prior Edit               |
         |                                      |
         | prior edit step [_______________|v]  |
         | mouse radius [____]                  |
         | power:  center [____]  edge [____]   |
         |                                      |
         |                             [OK] [X] |
         |______________________________________|

***/

   zd = zdialog_new(TX("Copy Prior Edit"),Mwin,"OK","X",null);
   CEF->zd = zd;

   zdialog_add_widget(zd,"hbox","hbstep","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labstep","hbstep",TX("prior edit step"),"space=5");
   zdialog_add_widget(zd,"combo","editstep","hbstep");
   zdialog_add_widget(zd,"hbox","hbr","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labr","hbr",TX("Mouse Radius"),"space=5");
   zdialog_add_widget(zd,"zspin","radius","hbr","2|500|1|200");
   zdialog_add_widget(zd,"hbox","hbt","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labtc","hbt",TX("power: center"),"space=5");
   zdialog_add_widget(zd,"zspin","center","hbt","0|100|1|50");
   zdialog_add_widget(zd,"label","labte","hbt",TX("Edge"),"space=5");
   zdialog_add_widget(zd,"zspin","edge","hbt","0|100|1|0");

   for (int ii = 0; ii <= URS_pos; ii++) {                                             //  prior edit steps > dropdown list
      snprintf(text,40,"%d %s",ii,URS_menu[ii]);                                       //  (includes current image, URS_pos 0)
      zdialog_stuff(zd,"editstep",text);
   }

   E9pxm = PXM_copy(E1pxm);                                                            //  initial source image (copy from)

   radius = 100;
   cpower = 50;
   epower = 0;

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


//  dialog event and completion function

int copy_prior_edit_dialog_event(zdialog *zd, ch *event)
{
   using namespace copy_prior_edit_names;

   void  copy_prior_edit_mousefunc();

   ch       text[40];
   int      nn;

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

   if (zd->zstat)                                                                      //  done or cancel
   {
      if (zd->zstat == 1) edit_done(0);                                                //  complete edit
      else edit_cancel(0);
      freeMouse();                                                                     //  disconnect mouse function
      return 1;
   }

   if (strmatch(event,"focus"))                                                        //  toggle mouse capture
      takeMouse(copy_prior_edit_mousefunc,0);

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

   if (strmatch(event,"center"))
      zdialog_fetch(zd,"center",cpower);                                               //  mouse center power

   if (strmatch(event,"edge"))
      zdialog_fetch(zd,"edge",epower);                                                 //  mouse edge power

   if (strmatch(event,"editstep"))                                                     //  select prior edit step
   {
      zdialog_fetch(zd,"editstep",text,40);                                            //  "nn menuname"
      nn = atoi(text);
      if (nn < 0 || nn > URS_max) return 1;
      if (E9pxm) PXM_free(E9pxm);
      E9pxm = load_undo(nn);                                                           //  load prior edit output image
      if (! E9pxm) return 1;
      if (E9pxm->ww != Eww || E9pxm->hh != Ehh) {
         zmessageACK(Mwin,TX("Image dimensions have changed \n"
                             "Copy Prior Edit cannot be used"));
         PXM_free(E9pxm);                                                              //  free memory
         E9pxm = 0;
      }
   }

   return 1;
}


//  mouse function

void copy_prior_edit_mousefunc()
{
   using namespace copy_prior_edit_names;

   int      px, py, ww, rx, ry, radius2;
   float    rad, rad2, power, F1;
   float    R0, G0, B0, R3, G3, B3, R9, G9, B9;
   float    *pix0, *pix3, *pix9;

   if (! CEF || ! E9pxm) return;                                                       //  no source image

   radius2 = radius * radius;

   draw_mousecircle(Mxposn,Myposn,radius,0,0);                                         //  show mouse selection circle

   if (! Mxdrag && ! Mydrag) return;                                                   //  wait for drag event
   Mxdrag = Mydrag = 0;                                                                //  neutralize drag

   for (rx = -radius; rx <= radius; rx++)                                              //  loop every pixel in radius
   for (ry = -radius; ry <= radius; ry++)
   {
      rad2 = rx * rx + ry * ry;
      if (rad2 > radius2) continue;                                                    //  outside radius
      px = Mxposn + rx;
      py = Myposn + ry;
      if (px < 0 || px > Eww-1) continue;                                              //  off the image edge
      if (py < 0 || py > Ehh-1) continue;

      rad = sqrt(rad2);
      power = cpower + rad / radius * (epower - cpower);                               //  power at pixel radius, 0 - 100
      F1 = 0.0002 * power;                                                             //  scale 0 - 0.02

      pix0 = PXMpix(E0pxm,px,py);                                                      //  original image pixel
      pix3 = PXMpix(E3pxm,px,py);                                                      //  current (painted) pixel
      pix9 = PXMpix(E9pxm,px,py);                                                      //  source image pixel

      R0 = pix0[0];                                                                    //  original pixel RGB
      G0 = pix0[1];
      B0 = pix0[2];

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

      R9 = pix9[0];                                                                    //  source pixel RGB
      G9 = pix9[1];
      B9 = pix9[2];

      if (Mbutton == 1) {                                                              //  left mouse, move to source image
         R3 += F1 * (R9 - R0);                                                         //  (paint)
         G3 += F1 * (G9 - G0);
         B3 += F1 * (B9 - B0);
      }

      else if (Mbutton == 3) {                                                         //  right mouse, move to original image
         R3 += F1 * (R0 - R9);                                                         //  (unpaint)
         G3 += F1 * (G0 - G9);
         B3 += F1 * (B0 - B9);
      }

      if (R0 > R9) {                                                                   //  limit R3 range to R0 ... R1
         if (R3 < R9) R3 = R9;
         else if (R3 > R0) R3 = R0;
      }
      else {
         if (R3 < R0) R3 = R0;
         else if (R3 > R9) R3 = R9;
      }

      if (G0 > G9) {                                                                   //  G3 range
         if (G3 < G9) G3 = G9;
         else if (G3 > G0) G3 = G0;
      }
      else {
         if (G3 < G0) G3 = G0;
         else if (G3 > G9) G3 = G9;
      }

      if (B0 > B9) {                                                                   //  B3 range
         if (B3 < B9) B3 = B9;
         else if (B3 > B0) B3 = B0;
      }
      else {
         if (B3 < B0) B3 = B0;
         else if (B3 > B9) B3 = B9;
      }

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

   px = Mxposn - radius - 1;                                                           //  repaint modified area
   py = Myposn - radius - 1;
   ww = 2 * radius + 3;
   Fpaint3(px,py,ww,ww,0);

   draw_mousecircle(Mxposn,Myposn,radius,0,0);                                         //  show mouse selection circle

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

   return;
}


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

//  minimal edit function to use as a template for coding new functions

namespace template_names
{
   editfunc    EFtemplate;
   float       userinput;                                                              //  user input value, -1 ... +1
}


//  menu function

void m_template(GtkWidget *, ch *menu)
{
   using namespace template_names;

   int    template_dialog_event(zdialog* zd, ch  *event);
   void   template_curvedit(int spc);
   void * template_thread(void *);

   GtkWidget   *drawwin_scale;                                                         //  drawing window for brightness scale

   printf("m_template \n");

   EFtemplate.menuname = "Template";
   EFtemplate.menufunc = m_template;
   EFtemplate.FprevReq = 1;                                                            //  use preview
   EFtemplate.Farea = 2;                                                               //  select area usable
   EFtemplate.Fpaintedits = 1;                                                         //  use with paint edits OK
   EFtemplate.Fscript = 1;                                                             //  scripting supported
   EFtemplate.threadfunc = template_thread;

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

/***
       _______________________________________________
      |                   Template                    |
      |  ___________________________________________  |
      | |                                           | |
      | |             curve edit area               | |
      | |                                           | |
      | | ----------------------------------------- | |                                //  editable function curve
      | |                                           | |
      | |                                           | |
      | |___________________________________________| |
      | |___________________________________________| |                                //  black to white brightness scale
      |                                               |
      | input  =================[]==================  |                                //  user input value
      |                                               |
      |                              [Reset] [OK] [X] |
      |_______________________________________________|

***/

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

   zdialog_add_widget(zd,"frame","frameH","dialog",0,"expand");                        //  edit-curve
   zdialog_add_widget(zd,"frame","frameB","dialog");                                   //  brightness scale

   zdialog_add_widget(zd,"hbox","hbsat","dialog",0,"space=5|expand");
   zdialog_add_widget(zd,"label","labsat","hbsat",TX("input"));
   zdialog_add_widget(zd,"hscale","userinput","hbsat","-1.0|1.0|0.01|0.0","expand");

   GtkWidget *frameH = zdialog_gtkwidget(zd,"frameH");                                 //  setup edit curves
   spldat *sd = splcurve_init(frameH,template_curvedit);
   EFtemplate.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 = overall brightness
   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

   GtkWidget *frameB = zdialog_gtkwidget(zd,"frameB");                                 //  setup drawing area
   drawwin_scale = gtk_drawing_area_new();
   gtk_container_add(GTK_CONTAINER(frameB),drawwin_scale);
   gtk_widget_set_size_request(drawwin_scale,100,12);
   G_SIGNAL(drawwin_scale,"draw",brightness_scale,0);

   userinput = 0;                                                                      //  neutral value

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

   return;
}


//  dialog event and completion callback function

int template_dialog_event(zdialog *zd, ch  *event)
{
   using namespace template_names;

   spldat      *sd = EFtemplate.sd;
   float       input0, dinput, dy;
   float       Fapply = 0;
   int         ii;

   if (strmatch(event,"done")) zd->zstat = 2;                                          //  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 (strmatch(event,"fullsize")) {                                                   //  from select area
      edit_fullsize();                                                                 //  get full size image
      thread_signal();
      thread_wait();                                                                   //  required for paint edits
      return 1;
   }

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

      zdialog_stuff(zd,"userinput",0);                                                 //  neutral value
      userinput = 0;

      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 curves

      edit_reset();
      return 1;
   }

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

      if (CEF->Fmods) {
         edit_fullsize();                                                              //  get full size image
         thread_signal();                                                              //  apply changes
         edit_done(0);                                                                 //  complete edit
      }

      else edit_cancel(0);                                                             //  no change
      return 1;
   }

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

   if (strmatch(event,"userinput"))                                                    //  input slider -1 ... +1
   {
      input0 = userinput;                                                              //  old value
      zdialog_fetch(zd,"userinput",userinput);                                         //  new value
      dinput = userinput - input0;                                                     //  change, + -

      for (ii = 0; ii < sd->nap[0]; ii++)                                              //  update curve 0 nodes from input value
      {
         dy = sd->apy[0][ii] + 0.5 * dinput;
         if (dy < 0) dy = 0;
         if (dy > 1) dy = 1;
         sd->apy[0][ii] = dy;
      }

      splcurve_generate(sd,0);                                                         //  regenerate curve 0
      gtk_widget_queue_draw(sd->drawarea);                                             //  draw curve 0
      Fapply = 1;
   }

   if (Fapply) thread_signal();                                                        //  update the image

   return 1;
}


//  this function is called when the curve is edited

void template_curvedit(int spc)
{
   using namespace template_names;
   thread_signal();
   return;
}


//  thread function

void * template_thread(void *arg)
{
   using namespace template_names;

   void * template_wthread(void *);

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

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

   do_wthreads(template_wthread,NSMP);

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

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


void * template_wthread(void *arg)                                                     //  worker thread function
{
   using namespace template_names;

   int         index = *((int *) arg);
   int         ii, Fend, px, py;
   float       *pix1, *pix3;
   float       R1, G1, B1, R3, G3, B3, R9, G9, B9;
   float       pixbrite, userinput;
   float       coeff = 1000.0 / 256.0;
   float       blend;
   spldat      *sd = EFtemplate.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 = R9 = pix1[0];                                                               //  input RGB values
      G1 = G9 = pix1[1];
      B1 = B9 = pix1[2];

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

      if (sd->mod[0])                                                                  //  curve was modified
      {
         pixbrite = 0.333 * (R3 + G3 + B3);                                            //  pixel brightness, 0 to 255.9
         ii = coeff * pixbrite;                                                        //  curve index 0-999
         if (ii < 0) ii = 0;
         if (ii > 999) ii = 999;
         userinput = 2.0 * sd->yval[0][ii] - 1.0;                                      //  -1 ... 0 ... +1

         R9 = R9 + userinput * (R9 - pixbrite);                                        //  new output RGB
         G9 = G9 + userinput * (G9 - pixbrite);
         B9 = B9 + userinput * (B9 - pixbrite);
      }

      RGBfix(R9,G9,B9);                                                                //  clamp values within 0-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;
}


