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

   Fotoxx      edit photos and manage collections

   Copyright 2007-2020 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.

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

   Fotoxx image edit - Edit menu functions

   m_trim_rotate           trim/crop and rotate combination
   m_upright               auto upright based on EXIF orientation data
   m_retouch               brightness, contrast, color ...
   blackbodyRGB            convert deg. K to RGB factors
   m_resize                resize (rescale) image
   m_adjust_RGB            adjust brightness/color using RGB or CMY colors
   m_adjust_HSL            adjust color using HSL model
   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_paint_image           paint on the image with a color
   RGB_chooser             select RGB color or use color chooser file
   HSL_chooser             select HSL color 
   m_copypixels1           copy pixels within one image
   m_copypixels2           copy pixels from source image to target image
   m_copypixels3           copypixels2 source image process
   m_paint_edits           paint edit function gradually with the mouse
   m_undo_edits            undo prior edits gradually with the mouse
   m_plugins               plugins menu function
   m_edit_plugins          add/revise/delete plugin menu functions
   m_run_plugin            run a plugin menu command and update image
   m_rawtherapee           edit a camera RAW file using this program

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

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

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

//  trim (crop) and/or rotate an image                                           //  combined crop and rotate function
//  Fotoxx 20.0:  extensive revisions for preview mode and other changes         //  20.0

//  fotoxx.h
//    int      trimx1, trimy1, trimx2, trimy2;                                   //  trim rectangle (calling function)
//    char     *trimsizes[10];                                                   //  previous trim sizes "NNNNxNNNN"
//    char     *trimbuttons[5];                                                  //  trim dialog button labels
//    char     *trimratios[5];                                                   //  corresponding aspect ratios


namespace trimrotate
{
   editfunc EFtrimrotate;
   zdialog  *zd;

   int      ptrimx1, ptrimy1, ptrimx2, ptrimy2;                                  //  prior trim rectangle 
   int      trimww, trimhh;                                                      //  trim rectangle width and height
   double   trimR;                                                               //  trim ratio, width/height
   double   Zratio;                                                              //  E0 size / E3 size   >= 1.0
   double   rotate_goal, rotate_angle;                                           //  target and actual rotation
   int      E0ww, E0hh, E3ww, E3hh;                                              //  full size image and preview size
   int      Fguidelines, guidelineX, guidelineY;                                 //  horz/vert guidelines for rotate
   int      Fcorner, Fside, Frotate;
   int      KBcorner, KBside;
   int      Ftimer1;
   double   Ftimer2;

   void  dialog();
   int   timerfunc(void *);
   int   dialog_event(zdialog *zd, cchar *event);
   void  trimsize_menu_event(GtkWidget *, cchar *menu);
   void  update_prev_trimsize();
   void  mousefunc();
   void  KBfunc(int key);
   void  trim_limits();
   void  trim_darkmargins();
   void  trim_final();
   void  rotate_func(int fast);
   void  drawlines(cairo_t *cr);
   void  autotrim();
   void  trim_customize();
}


//  menu function

void m_trim_rotate(GtkWidget *, cchar *menu)
{
   using namespace trimrotate;

   int         ii, xmargin, ymargin;
   cchar       *trim_message = E2X("drag middle to move \n"
                                   "drag corners to resize \n"
                                   "drag right edge to level");
   char        text[20];
   
   F1_help_topic = "trim/rotate";

   EFtrimrotate.menufunc = m_trim_rotate;                                        //  menu function
   EFtrimrotate.funcname = "trim/rotate";
   EFtrimrotate.menuname = menu;
   EFtrimrotate.Frestart = 1;                                                    //  allow restart
   EFtrimrotate.mousefunc = mousefunc;
   
   if (! zstrstr("keep max auto",menu))                                          //  if not special case,
      EFtrimrotate.FprevReq = 1;                                                 //    use preview (small) size

   if (! edit_setup(EFtrimrotate)) return;                                       //  setup edit
   
   PXM_addalpha(E0pxm);
   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

   E0ww = E0pxm->ww;                                                             //  full-size image dimensions
   E0hh = E0pxm->hh;
   E3ww = E3pxm->ww;                                                             //  preview image dimensions
   E3hh = E3pxm->hh;
   
   Zratio = 1.0 * (E0ww + E0hh) / (E3ww + E3hh);                                 //  full / preview size  >= 1.0

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

   if (menu && strmatch(menu,"keep")) {                                          //  use a preset trim rectangle (1x image))
      if (trimx1 < 0) trimx1 = 0;
      if (trimx2 > E3ww) trimx2 = E3ww;                                          //  sanity checks
      if (trimx2 <= trimx1) {
         trimx1 = 0.2 * E3ww;
         trimx2 = 0.8 * E3ww;
      }
      if (trimy1 < 0) trimy1 = 0;
      if (trimy2 > E3hh) trimy2 = E3hh;
      if (trimy2 <= trimy1) {
         trimy1 = 0.2 * E3hh;
         trimy2 = 0.8 * E3hh;
      }
      trimww = trimx2 - trimx1;
      trimhh = trimy2 - trimy1;
      trimR = 1.0 * trimww / trimhh;                                             //  trim ratio = width/height
   }
   
   else if (menu && strmatch(menu,"max")) {
      trimww = E3ww;                                                             //  initial trim rectangle, 1x image size
      trimhh = E3hh;
      xmargin = ymargin = 0;
      trimx1 = 0;
      trimx2 = E3ww;
      trimy1 = 0;
      trimy2 = E3hh;
      trimR = 1.0 * E3ww / E3hh;                                                 //  trim ratio = width/height
   }   

   else
   {
      trimww = 0.8 * E3ww;                                                       //  initial trim rectangle, 80% image size
      trimhh = 0.8 * E3hh;
      xmargin = 0.5 * (E3ww - trimww);                                           //  set balanced margins
      ymargin = 0.5 * (E3hh - trimhh);
      trimx1 = xmargin;
      trimx2 = E3ww - xmargin;
      trimy1 = ymargin;
      trimy2 = E3hh - ymargin;
      trimR = 1.0 * trimww / trimhh;                                             //  trim ratio = width/height
   }

   ptrimx1 = 0;                                                                  //  set prior trim rectangle
   ptrimy1 = 0;                                                                  //    = 100% of image
   ptrimx2 = E3ww;
   ptrimy2 = E3hh;
   
   rotate_goal = rotate_angle = 0;                                               //  initially no rotation

/***
       ______________________________________
      |                                      |
      |     drag middle to move              |
      |     drag corners to resize           |
      |     drag right edge to level         |
      | - - - - - - - - - - - - - - - - - -  |
      |  width [____]  height [____]         |
      |  Ratio 1.5   [x] Lock Ratio          |
      |  Trim Size: [Max] [Auto] [Prev]      |
      | - - - - - - - - - - - - - - - - - -  |
      |  Set Ratio  [Invert]  [Customize]    |
      |  [1:1] [2:1] [3:2] [4:3] [16:9]      |
      | - - - - - - - - - - - - - - - - - -  |
      |  Degrees [___]  [Auto Level]         |
      |  [-90°]  [+90°]  [180°]  [upright]   |
      |                                      |
      |               [Grid] [Done] [Cancel] |
      |______________________________________|

***/

   zd = zdialog_new(E2X("Trim/Rotate"),Mwin,Bgrid,Bdone,Bcancel,null);
   EFtrimrotate.zd = zd;

   zdialog_add_widget(zd,"label","labtrim","dialog",trim_message,"space=3");

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

   zdialog_add_widget(zd,"hbox","hbwh","dialog");
   zdialog_add_widget(zd,"label","labW","hbwh",Bwidth,"space=5");
   zdialog_add_widget(zd,"zspin","width","hbwh","20|30000|1|1000");              //  fotoxx.h
   zdialog_add_widget(zd,"label","space","hbwh",0,"space=5");
   zdialog_add_widget(zd,"label","labH","hbwh",Bheight,"space=5");
   zdialog_add_widget(zd,"zspin","height","hbwh","20|30000|1|600");

   zdialog_add_widget(zd,"hbox","hbrat","dialog");
   zdialog_add_widget(zd,"label","labR","hbrat",E2X("ratio"),"space=5");
   zdialog_add_widget(zd,"label","ratio","hbrat","1.67   ");
   zdialog_add_widget(zd,"check","lock","hbrat",E2X("Lock Ratio"),"space=15");

   zdialog_add_widget(zd,"hbox","hbsz","dialog");
   zdialog_add_widget(zd,"label","labsz","hbsz",E2X("Trim Size:"),"space=5");
   zdialog_add_widget(zd,"button","max","hbsz",Bmax,"space=5");
   zdialog_add_widget(zd,"button","auto","hbsz",Bauto,"space=5");
   zdialog_add_widget(zd,"button","prev","hbsz",Bprev,"space=5");

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

   zdialog_add_widget(zd,"hbox","hbsr1","dialog");
   zdialog_add_widget(zd,"label","labsr1","hbsr1",E2X("Set Ratio"),"space=5");
   zdialog_add_widget(zd,"button","invert","hbsr1",E2X("Invert"),"space=5");
   zdialog_add_widget(zd,"button","custom","hbsr1",E2X("Customize"),"space=5");

   zdialog_add_widget(zd,"hbox","hbsr2","dialog",0,"space=2");
   for (ii = 0; ii < 5; ii++)
      zdialog_add_widget(zd,"button",trimbuttons[ii],"hbsr2",trimbuttons[ii],"space=5");
   
   zdialog_add_widget(zd,"hsep","sep3","dialog",0,"space=5");

   zdialog_add_widget(zd,"hbox","hbrot","dialog");
   zdialog_add_widget(zd,"label","labrotate","hbrot",E2X("Degrees:"),"space=5");
   zdialog_add_widget(zd,"zspin","degrees","hbrot","-180|180|0.1|0");
   zdialog_add_widget(zd,"button","level","hbrot",E2X("Auto Level"),"space=10");

   zdialog_add_widget(zd,"hbox","hb90","dialog",0,"space=5");
   zdialog_add_widget(zd,"imagebutt","-90","hb90","rotate-left.png","size=32|space=8");
   zdialog_add_widget(zd,"imagebutt","+90","hb90","rotate-right.png","size=32|space=8");
   zdialog_add_widget(zd,"imagebutt","180","hb90","rotate-180.png","size=32|space=8");
   zdialog_add_widget(zd,"button","upright","hb90",E2X("Upright"),"space=8");

   zdialog_add_ttip(zd,"max",E2X("maximize trim box"));
   zdialog_add_ttip(zd,"auto",E2X("trim transparent edges"));
   zdialog_add_ttip(zd,"lock",E2X("lock width/height ratio"));
   zdialog_add_ttip(zd,"level",E2X("use EXIF data if available"));
   zdialog_add_ttip(zd,"prev",Bprevtip); 

   zd = EFtrimrotate.zd;

   zdialog_restore_inputs(zd);                                                   //  restore all prior inputs

   zdialog_stuff(zd,"width",trimww*Zratio);                                      //  stuff width, height, ratio as
   zdialog_stuff(zd,"height",trimhh*Zratio);                                     //    pre-calculated for this image
   snprintf(text,20,"%.3f  ",trimR);
   zdialog_stuff(zd,"ratio",text);
   zdialog_stuff(zd,"degrees",0);

   if (zstrstr("keep max auto",menu))                                            //  no ratio lock in these cases
      zdialog_stuff(zd,"lock",0);

   takeMouse(mousefunc,dragcursor);                                              //  connect mouse function
   currgrid = 1;                                                                 //  use trim/rotate grid
   Fzoom = 0;
   trim_darkmargins();

   zdialog_run(zd,dialog_event,"save");                                          //  run dialog - parallel
   
   if (menu && strmatch(menu,"auto")) zdialog_send_event(zd,"auto");

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

   return;
}


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

int trimrotate::timerfunc(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);                                                               //  do high quality rotate
   return 1;
}


//  dialog event and completion callback function

int trimrotate::dialog_event(zdialog *zd, cchar *event)
{
   using namespace trimrotate;

   static GtkWidget  *popmenu = 0;

   static int  flip = 0;
   int         width, height, delta;
   int         ii, Rlock;
   double      r1, r2, Fratio = 0;
   int         angle = 0;
   char        orientation = 0;
   char        text[20];
   cchar       *pp;
   cchar       *exifkey[1];
   char        *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
      edit_cancel(0);
      m_trim_rotate(0,0);
      return 1;
   }
   
   Fcorner = Fside = KBcorner = KBside = Frotate = 0;                            //  nothing underway

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

   if (zd->zstat)                                                                //  dialog complete
   {
      if (zd->zstat == 1) {                                                      //  [grid]
         zd->zstat = 0;                                                          //  keep dialog active
         if (! gridsettings[1][GON]) m_gridlines(0,"grid 1");
         else toggle_grid(2);
         return 1;
      }

      if (zd->zstat == 2)                                                        //  [done]
      {
         Ftimer1 = Ftimer2 = 0;                                                  //  stop watchdog timer
         draw_toplines(2,0);                                                     //  erase trim rectangle
         erase_topcircles();                                                     //  erase center circle             
         currgrid = 0;                                                           //  restore normal grid settings

         edit_fullsize();                                                        //  get full size image

         E3ww = E3pxm->ww;                                                       //  rescale all settings
         E3hh = E3pxm->hh;
         trimx1 *= Zratio;
         trimy1 *= Zratio;
         trimx2 *= Zratio;
         trimy2 *= Zratio;
         Zratio = 1.0;
         
         zdialog_fetch(zd,"width",trimww);                                       //  width/height set by user
         zdialog_fetch(zd,"height",trimhh);

         trim_final();                                                           //  rotate/trim final image

         CEF->Fmods++;                                                           //  finish edit 
         edit_done(0);

         update_prev_trimsize();                                                 //  update prev. trim size memory
         return 1;
      }

      Ftimer1 = Ftimer2 = 0;                                                     //  stop watchdog timer
      draw_toplines(2,0);                                                        //  [cancel] - erase trim rectangle
      erase_topcircles();                                                        //  erase center circle             
      currgrid = 0;                                                              //  restore normal grid settings
      edit_cancel(0);
      return 1;
   }

   if (strmatch(event,"auto"))                                                   //  auto trim margins
   {
      autotrim();                                                                //  do auto-trim
      zdialog_stuff(zd,"width",trimww*Zratio);                                   //  update dialog values
      zdialog_stuff(zd,"height",trimhh*Zratio);
      zdialog_stuff(zd,"lock",0);                                                //  set no ratio lock
      trim_darkmargins();                                                        //  show trim area in image
      Fpaintnow();
      return 1;
   }

   if (strmatch(event,"max"))                                                    //  maximize trim rectangle
   {
      trimx1 = trimy1 = 0;
      trimx2 = trimww = E3ww;
      trimy2 = trimhh = E3hh;
      zdialog_stuff(zd,"width",E3ww*Zratio);                                     //  stuff width, height
      zdialog_stuff(zd,"height",E3hh*Zratio);
      zdialog_stuff(zd,"lock",0);                                                //  set no ratio lock
      trim_darkmargins();                                                        //  show trim area in image
      Fpaintnow();
      return 1;
   }

   if (zstrstr("width height",event))                                            //  width or height input
   {
      zdialog_fetch(zd,"width",width);                                           //  full image scale, E0
      zdialog_fetch(zd,"height",height);

      if (width < 20) width = 20;
      if (width > E3ww*Zratio) width = E3ww*Zratio;
      if (height < 20) height = 20;
      if (height > E3hh*Zratio) height = E3hh*Zratio;

      zdialog_fetch(zd,"lock",Rlock);                                            //  get ratio lock, on/off
      if (Rlock) {
         if (strmatch(event,"width")) 
            height = width / trimR + 0.5;
         if (strmatch(event,"height")) 
            width = height * trimR + 0.5;
         if (width > E3ww*Zratio) {
            width = E3ww*Zratio;
            height = width / trimR + 0.5;
         }
         if (height > E3hh*Zratio) {
            height = E3hh*Zratio;
            width = height * trimR + 0.5;
         }
         if (width > E3ww*Zratio) width = E3ww*Zratio;                           //  rounding could put it over limit
         if (height > E3hh*Zratio) height = E3hh*Zratio;
      }
      
      zdialog_stuff(zd,"width",width);
      zdialog_stuff(zd,"height",height);
   
      flip = 1 - flip;                                                           //  alternates 0, 1, 0, 1 ...

      delta = width / Zratio - trimww;                                           //  change in E3 width

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

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

      if (trimx1 < 0) trimx1 = 0;                                                //  keep within limits
      if (trimx2 > E3ww) trimx2 = E3ww;                                          //  use ww not ww-1
      trimww = trimx2 - trimx1;

      delta = height / Zratio - trimhh;                                          //  change in E3 height

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

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

      if (trimy1 < 0) trimy1 = 0;                                                //  keep within limits
      if (trimy2 > E3hh) trimy2 = E3hh;                                          //  use ww not ww-1
      trimhh = trimy2 - trimy1;

      trim_darkmargins();                                                        //  show trim area in image
      return 1;
   }

   zdialog_fetch(zd,"lock",Rlock);                                               //  get ratio lock, on/off
   if (! Rlock) trimR = 1.0 * trimww / trimhh;
   snprintf(text,20,"%.3f  ",trimR);                                             //  update ratio
   zdialog_stuff(zd,"ratio",text);

   for (ii = 0; ii < 5; ii++)                                                    //  trim ratio buttons
      if (strmatch(event,trimbuttons[ii])) break;
   if (ii < 5) {
      r1 = r2 = Fratio = 0;                                                      //  ratio button pressed
      pp = strField(trimratios[ii],':',1);
      if (pp) r1 = atof(pp);
      pp = strField(trimratios[ii],':',2);
      if (pp) r2 = atof(pp);
      if (r1 > 0 && r2 > 0) Fratio = r1/r2;
      if (Fratio < 0.1 || Fratio > 10) Fratio = 1.0;
      if (! Fratio) return 1;
      zdialog_stuff(zd,"lock",1);                                                //  assume lock is wanted
   }
   
   if (strmatch(event,"invert"))                                                 //  [invert] ratio button
      Fratio = 1.0 * trimhh / trimww;

   if (Fratio)                                                                   //  ratio was changed
   {
      trimR = Fratio;

      if (trimx2 - trimx1 > trimy2 - trimy1)                                     //  adjust smaller dimension
         trimy2 = trimy1 + (trimx2 - trimx1) / trimR + 0.5;                      //  round
      else
         trimx2 = trimx1 + (trimy2 - trimy1) * trimR + 0.5;

      if (trimx2 > E3ww) {                                                       //  if off the right edge,
         trimx2 = E3ww;                                                          //  adjust height
         trimy2 = trimy1 + (trimx2 - trimx1) / trimR + 0.5;
      }

      if (trimy2 > E3hh) {                                                       //  if off the bottom edge,
         trimy2 = E3hh;                                                          //  adjust width
         trimx2 = trimx1 + (trimy2 - trimy1) * trimR + 0.5;
      }

      trimww = trimx2 - trimx1;                                                  //  new rectangle dimensions
      trimhh = trimy2 - trimy1;

      zdialog_stuff(zd,"width",trimww*Zratio);                                   //  stuff width, height, ratio
      zdialog_stuff(zd,"height",trimhh*Zratio);
      snprintf(text,20,"%.3f  ",trimR);
      zdialog_stuff(zd,"ratio",text);

      trim_darkmargins();                                                        //  update trim area in image
      return 1;
   }
   
   if (zstrstr("+90 -90 180",event))                                             //  rotate action
   {
      if (strmatch(event,"+90"))                                                 //  90 deg. CW
         rotate_goal += 90;

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

      if (strmatch(event,"180"))                                                 //  180 deg. (upside down)
         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);                                                            //  E3 is rotated E1
      
      if (zstrstr("+90 -90",event))                                              //  adjust margins                     20.0
         dialog_event(zd,"auto");
   }

   if (strmatch(event,"upright"))                                                //  auto upright button
   {
      exifkey[0] = exif_orientation_key;
      exif_get(curr_file,exifkey,ppv,1);                                         //  get EXIF: Orientation
      if (ppv[0]) {
         orientation = *ppv[0];                                                  //  single character
         zfree(ppv[0]);
      }

      if (orientation == '6') angle = +90;                                       //  rotate clockwise 90 deg.
      if (orientation == '8') angle = -90;                                       //  counterclockwise 90 deg.
      if (orientation == '3') angle = 180;                                       //  180 deg.
      if (! angle) {
         zmessageACK(Mwin,E2X("rotation unknown"));
         return 1;
      }

      rotate_goal += angle;
      rotate_func(0);
   }

   if (strmatch(event,"degrees"))                                                //  rotate action
   {
      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
   }

   if (strmatch(event,"level"))                                                  //  auto level using EXIF RollAngle
   {
      exifkey[0] = exif_rollangle_key;
      exif_get(curr_file,exifkey,ppv,1);
      if (ppv[0]) {
         rotate_goal = atof(ppv[0]);
         zfree(ppv[0]);
         rotate_func(0);                                                         //  E3 is rotated E1
      }
   }
   
   if (strmatch(event,"prev"))                                                   //  [Prev] button - get prev. trim size
   {
      zdialog_stuff(zd,"lock",0);
      if (popmenu) gtk_widget_destroy(popmenu);
      popmenu = create_popmenu();                                                //  make popup menu with trim sizes
      for (int ii = 0; ii < 10; ii++)
         if (trimsizes[ii]) 
            add_popmenu_item(popmenu,trimsizes[ii],trimsize_menu_event,0,0);
      popup_menu(Mwin,popmenu);
      return 1;
   }

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

   return 1;
}


//  trim size popup menu event function

void trimrotate::trimsize_menu_event(GtkWidget *, cchar *menu)
{
   using namespace trimrotate;

   int         nn, width, height;
   zdialog     *zd;

   nn = sscanf(menu,"%dx%d",&width,&height);                                     //  chosen trim size, "NNNNxNNNN"
   if (nn != 2) return;
   zd = EFtrimrotate.zd;
   zdialog_stuff(zd,"width",width);                                              //  update dialog
   zdialog_stuff(zd,"height",height);
   zdialog_send_event(zd,"width");
   return;
}


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

void trimrotate::update_prev_trimsize()
{
   using namespace trimrotate;

   int      ii;
   char     mysize[20];
   
   snprintf(mysize,20,"%dx%d",trimww,trimhh);                                    //  this trim size used, "NNNNxNNNN"

   for (ii = 0; ii < 10; ii++) {
      if (! trimsizes[ii]) break;                                                //  check past sizes list for same size
      if (strmatch(trimsizes[ii],mysize)) break;
   }
   
   if (ii < 10 && trimsizes[ii]) {                                               //  found in list
      zfree(trimsizes[ii]);                                                      //  remove from list
      for (ii++; ii < 10; ii++)                                                  //  pack down remaining entries
         trimsizes[ii-1] = trimsizes[ii];
      trimsizes[9] = 0;                                                          //  last = null
   }
   
   if (trimsizes[9]) zfree(trimsizes[9]);                                        //  if entry 9 exists, discard
   for (ii = 9; ii > 0; ii--)                                                    //  move entries 0-8 to 1-9
      trimsizes[ii] = trimsizes[ii-1];
   trimsizes[0] = zstrdup(mysize);                                               //  entry 0 = this size
   return;
}


//  trim/rotate mouse function

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

   int      mpx, mpy, xdrag, ydrag;
   int      moveall = 0;
   int      dx, dy, dd, dc, ds;
   int      d1, d2, d3, d4;
   int      edgedist, linedist;
   zdialog  *zd = EFtrimrotate.zd;

   if (! LMclick && ! RMclick && ! Mxdrag && ! Mydrag) {                         //  no click or drag
      Fcorner = Fside = Frotate = 0;
      return;
   }
   
   mpx = mpy = xdrag = ydrag = 0;                                                //  avoid gcc warnings

   if (LMclick)                                                                  //  add vertical and horizontal
   {                                                                             //    lines at click position
      LMclick = 0;
      Fcorner = Fside = KBcorner = KBside = Frotate = 0;
      Fguidelines = 1;
      guidelineX = Mxclick;
      guidelineY = Myclick;
      trim_darkmargins();
      Fpaint2();
      return;
   }

   if (RMclick)                                                                  //  remove the lines
   {
      RMclick = 0;
      Fcorner = Fside = KBcorner = KBside = Frotate = 0;
      Fguidelines = 0;
      trim_darkmargins();
      Fpaint2();
      return;
   }

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

   if (Frotate) goto mouse_rotate;                                               //  continue rotate in progress

   else if (Fcorner)
   {
      if (Fcorner == 1) { trimx1 = mpx; trimy1 = mpy; }                          //  continue dragging corner with mouse
      if (Fcorner == 2) { trimx2 = mpx; trimy1 = mpy; }
      if (Fcorner == 3) { trimx2 = mpx; trimy2 = mpy; }
      if (Fcorner == 4) { trimx1 = mpx; trimy2 = mpy; }
   }
   
   else if (Fside)
   {
      if (Fside == 1) trimx1 = mpx;                                              //  continue dragging side with mouse
      if (Fside == 2) trimy1 = mpy;
      if (Fside == 3) trimx2 = mpx;
      if (Fside == 4) trimy2 = mpy;
   }

   else
   {
      moveall = 1;
      dd = 0.2 * (trimx2 - trimx1);                                              //  test if mouse is in the broad
      if (mpx < trimx1 + dd) moveall = 0;                                        //    middle of the rectangle
      if (mpx > trimx2 - dd) moveall = 0;
      dd = 0.2 * (trimy2 - trimy1);
      if (mpy < trimy1 + dd) moveall = 0;
      if (mpy > trimy2 - dd) moveall = 0;
   }

   if (moveall) {                                                                //  yes, move the whole rectangle
      trimx1 += xdrag;
      trimx2 += xdrag;
      trimy1 += ydrag;
      trimy2 += ydrag;
      Fcorner = Fside = KBcorner = KBside = Frotate = 0;
   }

   else if (! Fcorner && ! Fside)                                                //  no, find closest corner/side to mouse
   {
      dx = mpx - trimx1;
      dy = mpy - trimy1;
      d1 = sqrt(dx*dx + dy*dy);                                                  //  distance from NW corner

      dx = mpx - trimx2;
      dy = mpy - trimy1;
      d2 = sqrt(dx*dx + dy*dy);                                                  //  NE

      dx = mpx - trimx2;
      dy = mpy - trimy2;
      d3 = sqrt(dx*dx + dy*dy);                                                  //  SE

      dx = mpx - trimx1;
      dy = mpy - trimy2;
      d4 = sqrt(dx*dx + dy*dy);                                                  //  SW

      Fcorner = 1;                                                               //  find closest corner
      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

      dx = mpx - trimx1;
      dy = mpy - (trimy1 + trimy2) / 2;
      d1 = sqrt(dx*dx + dy*dy);                                                  //  distance from left side middle

      dx = mpx - (trimx1 + trimx2) / 2;
      dy = mpy - trimy1;
      d2 = sqrt(dx*dx + dy*dy);                                                  //  top middle

      dx = mpx - trimx2;
      dy = mpy - (trimy1 + trimy2) / 2;
      d3 = sqrt(dx*dx + dy*dy);                                                  //  right side middle

      dx = mpx - (trimx1 + trimx2) / 2;
      dy = mpy - trimy2;
      d4 = sqrt(dx*dx + dy*dy);                                                  //  bottom middle

      Fside = 1;                                                                 //  find closest side
      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

      if (dc < ds) Fside = 0;                                                    //  closer to corner
      else Fcorner = 0;                                                          //  closer to side
      
      if (Fside == 3) {                                                          //  closest to right side
         linedist = Dorgx + Mscale * trimx2 - Mwxposn;                           //  dist. to trim rect. right side
         edgedist = Dorgx + dww - Mwxposn;                                       //  dist. to image right edge          20.06
         if (abs(linedist) > abs(edgedist)) goto mouse_rotate;                   //  closer to image edge - rotate
      }                                                                          //  closer to trim rectangle - move it

      if (Fcorner == 1) { trimx1 = mpx; trimy1 = mpy; }                          //  move this corner to mouse
      if (Fcorner == 2) { trimx2 = mpx; trimy1 = mpy; }
      if (Fcorner == 3) { trimx2 = mpx; trimy2 = mpy; }
      if (Fcorner == 4) { trimx1 = mpx; trimy2 = mpy; }
      
      if (Fside == 1) trimx1 = mpx;                                              //  move this side to mouse
      if (Fside == 2) trimy1 = mpy;
      if (Fside == 3) trimx2 = mpx;
      if (Fside == 4) trimy2 = mpy;
      
      KBcorner = Fcorner;                                                        //  save last corner/side moved
      KBside = Fside;
   }

   trim_limits();                                                                //  check margin limits and adjust if req.
   trim_darkmargins();                                                           //  show trim area in image

   return;

// -------------------------

   mouse_rotate:

   Frotate = 1;
   Fcorner = Fside = KBcorner = KBside = 0;

   rotate_goal += 40.0 * ydrag / E3ww;                                           //  convert radians to degrees (reduced)
   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;
}


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

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

   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
      trimx1 += xstep;
      trimy1 += ystep;
   }

   if (KBcorner == 2) {                                                          //  NE
      trimx2 += xstep;
      trimy1 += ystep;
   }

   if (KBcorner == 3) {                                                          //  SE
      trimx2 += xstep;
      trimy2 += ystep;
   }

   if (KBcorner == 4) {                                                          //  SW
      trimx1 += xstep;
      trimy2 += ystep;
   }
   
   if (KBside == 1) trimx1 += xstep;                                             //  left
   if (KBside == 2) trimy1 += ystep;                                             //  top
   if (KBside == 3) trimx2 += xstep;                                             //  right
   if (KBside == 4) trimy2 += ystep;                                             //  bottom
   
   trim_limits();                                                                //  check margin limits and adjust if req.
   trim_darkmargins();                                                           //  show trim area in image
   return;
}


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

void trimrotate::trim_limits()
{
   using namespace trimrotate;

   int         Rlock, chop;
   int         ww, hh, ww2, hh2;
   float       drr;
   char        text[20];
   zdialog     *zd = EFtrimrotate.zd;

   if (trimx1 > trimx2-10) trimx1 = trimx2-10;                                   //  sanity limits
   if (trimy1 > trimy2-10) trimy1 = trimy2-10;

   zdialog_fetch(zd,"lock",Rlock);                                               //  w/h ratio locked
   if (Rlock && Fcorner) {
      if (Fcorner < 3)
         trimy1 = trimy2 - 1.0 * (trimx2 - trimx1) / trimR + 0.5;                //  round
      else
         trimy2 = trimy1 + 1.0 * (trimx2 - trimx1) / trimR + 0.5;
   }
   
   if (Rlock && Fside) {
      ww = trimx2 - trimx1;
      hh = trimy2 - trimy1;
      ww2 = ww;
      hh2 = hh;
      if (Fside == 1 || Fside == 3) hh2 = ww / trimR + 0.5;
      else ww2 = hh * trimR + 0.5;
      ww2 = (ww2 - ww) / 2;
      hh2 = (hh2 - hh) / 2;
      trimx1 -= ww2;
      trimx2 += ww2;
      trimy1 -= hh2;
      trimy2 += hh2;
   }

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

   if (trimx2 > E3ww) {
      trimx2 = E3ww;
      chop = 2;
   }

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

   if (trimy2 > E3hh) {
      trimy2 = E3hh;
      chop = 4;
   }

   if (Rlock && chop) {                                                          //  keep ratio if off edge
      if (chop < 3)
         trimy2 = trimy1 + 1.0 * (trimx2 - trimx1) / trimR + 0.5;                //  round
      else
         trimx2 = trimx1 + 1.0 * (trimy2 - trimy1) * trimR + 0.5;
   }

   if (trimx1 > trimx2-10) trimx1 = trimx2-10;                                   //  sanity limits
   if (trimy1 > trimy2-10) trimy1 = trimy2-10;

   if (trimx1 < 0) trimx1 = 0;                                                   //  keep within visible area
   if (trimx2 > E3ww) trimx2 = E3ww;
   if (trimy1 < 0) trimy1 = 0;
   if (trimy2 > E3hh) trimy2 = E3hh;

   trimww = trimx2 - trimx1;                                                     //  new rectangle dimensions
   trimhh = trimy2 - trimy1;

   drr = 1.0 * trimww / trimhh;                                                  //  new w/h ratio
   if (! Rlock) trimR = drr;

   zdialog_stuff(zd,"width",trimww*Zratio);                                      //  stuff width, height, ratio
   zdialog_stuff(zd,"height",trimhh*Zratio);
   snprintf(text,20,"%.3f  ",trimR);
   zdialog_stuff(zd,"ratio",text);

   return;
}


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

void trimrotate::trim_darkmargins()
{
   using namespace trimrotate;

   int      ox1, oy1, ox2, oy2;                                                  //  outer trim rectangle
   int      nx1, ny1, nx2, ny2;                                                  //  inner trim rectangle
   int      px, py;
   float    *pix1, *pix3;
   int      nc = E1pxm->nc, pcc = nc * sizeof(float);
   int      Rflag = 0;
   int      cx, cy, rad, rad2;
   
   if (trimx1 < 0) trimx1 = 0;                                                   //  keep within visible area
   if (trimx2 > E3ww) trimx2 = E3ww;
   if (trimy1 < 0) trimy1 = 0;
   if (trimy2 > E3hh) trimy2 = E3hh;

   if (ptrimx1 < trimx1) ox1 = ptrimx1;                                          //  outer rectangle
   else ox1 = trimx1;
   if (ptrimx2 > trimx2) ox2 = ptrimx2;
   else ox2 = trimx2;
   if (ptrimy1 < trimy1) oy1 = ptrimy1;
   else oy1 = trimy1;
   if (ptrimy2 > trimy2) oy2 = ptrimy2;
   else oy2 = trimy2;

   if (ptrimx1 > trimx1) nx1 = ptrimx1;                                          //  inner rectangle
   else nx1 = trimx1;
   if (ptrimx2 < trimx2) nx2 = ptrimx2;
   else nx2 = trimx2;
   if (ptrimy1 > trimy1) ny1 = ptrimy1;
   else ny1 = trimy1;
   if (ptrimy2 < trimy2) ny2 = ptrimy2;
   else ny2 = trimy2;

   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 > E3ww) ox2 = E3ww;
   if (oy2 > E3hh) oy2 = E3hh;
   
   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);

   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 < trimx1 || px >= trimx2 ||
          py < trimy1 || py >= trimy2) {
         pix3[0] = pix1[0] / 2;                                                  //  outside trim 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 < trimx1 || px >= trimx2 || 
          py < trimy1 || py >= trimy2) {
         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 < trimx1 || px >= trimx2 || 
          py < trimy1 || py >= trimy2) {
         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 < trimx1 || px >= trimx2 || py < trimy1 || py >= trimy2) {
         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 trim margin lines

   erase_topcircles();                                                           //  erase center circle

   rad = 15 / Mscale;                                                            //  repaint center area
   rad2 = 2 * rad + 1;
   cx = (ptrimx1 + ptrimx2) / 2;
   cy = (ptrimy1 + ptrimy2) / 2;
   Fpaint3(cx-rad,cy-rad,rad2,rad2,cr);

   cx = (trimx1 + trimx2) / 2;                                                   //  draw center circle
   cy = (trimy1 + trimy2) / 2;
   add_topcircle(cx,cy,10);
   draw_topcircles(cr);

   draw_context_destroy(draw_context); 
   
   ptrimx1 = trimx1;                                                             //  set prior trim rectangle
   ptrimx2 = trimx2;                                                             //    from current trim rectangle
   ptrimy1 = trimy1;
   ptrimy2 = trimy2;

   if (gridsettings[currgrid][GON]) Rflag++;                                     //  refresh gridlines
   if (ox1 == 0 || oy1 == 0 || ox2 == E3ww || oy2 == E3hh) Rflag++;              //  insure edges are refreshed
   if (Rflag) Fpaint2();                                                         //  refresh entire image

   return;
}


//  final trim - cut margins off
//  E3 is the input image, rotated
//  E3 is the output image from user trim rectangle

void trimrotate::trim_final()
{
   using namespace trimrotate;

   int      px1, py1, px2, py2;
   float    *pix3, *pix9;
   int      nc = E3pxm->nc, pcc = nc * sizeof(float);
   int      ww, hh, nn;
   
   ww = trimww;                                                                  //  save final width/height in user dialog
   hh = trimhh;
   
   if (rotate_angle != 0) {                                                      //  final rotate
      rotate_angle = 0;                                                          //  insure goal difference
      rotate_func(0);                                                            //  slow high quality rotate
      sharp_GR_callable(50,10);                                                  //  E3 = sharpened E3
   }

   nn = ww - (trimx2 - trimx1);                                                  //  adjust trim rectangle to hit
   trimx1 -= nn/2;                                                               //    target width and height
   if (trimx1 < 0) trimx1 = 0;
   trimx2 = trimx1 + ww;
   if (trimx2 > E3ww) trimx2 = E3ww;
   
   nn = hh - (trimy2 - trimy1);
   trimy1 -= nn/2;
   if (trimy1 < 0) trimy1 = 0;
   trimy2 = trimy1 + hh;
   if (trimy2 > E3hh) trimy2 = E3hh;

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

   for (py1 = trimy1; py1 < trimy2; py1++)                                       //  copy E3 (rotated) to new size
   for (px1 = trimx1; px1 < trimx2; px1++)
   {
      px2 = px1 - trimx1;
      py2 = py1 - trimy1;
      pix3 = PXMpix(E3pxm,px1,py1);
      pix9 = PXMpix(E9pxm,px2,py2);
      memcpy(pix9,pix3,pcc);
   }

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

   E3ww = E3pxm->ww;                                                             //  update E3 dimensions
   E3hh = E3pxm->hh;
   
   CEF->Fmods++;
   CEF->Fsaved = 0;

   Fpaint2();
   return;
}


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

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

   zdialog     *zd = EFtrimrotate.zd;
   
   if (rotate_goal == rotate_angle) return;
   
   if (fabs(rotate_goal) < 0.01) {                                               //  no rotation
      rotate_goal = rotate_angle = 0.0;
      zdialog_stuff(zd,"degrees",0.0);
      PXM_free(E3pxm);                                                           //  E1 >> E3
      E3pxm = PXM_copy(E1pxm);
   }

   PXM_free(E3pxm);
   E3pxm = PXM_rotate(E1pxm,rotate_goal,fast);                                   //  fast low quality rotate
   rotate_angle = rotate_goal;

   if (fast) Ftimer2 = get_seconds();                                            //  flag watchdog timer                20.0
   else Ftimer2 = 0;

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

   E3ww = E3pxm->ww;                                                             //  update E3 dimensions
   E3hh = E3pxm->hh;

   if (trimx2 > E3ww) trimx2 = E3ww;                                             //  contract trim rectangle if needed
   if (trimy2 > E3hh) trimy2 = E3hh;

   trimww = trimx2 - trimx1;                                                     //  new rectangle dimensions
   trimhh = trimy2 - trimy1;

   zdialog_stuff(zd,"width",trimww*Zratio);                                      //  stuff width, height
   zdialog_stuff(zd,"height",trimhh*Zratio);

   ptrimx1 = 0;                                                                  //  set prior trim rectangle
   ptrimy1 = 0;
   ptrimx2 = E3ww;                                                               //    = 100% of image
   ptrimy2 = E3hh;
   
   CEF->Fmods++;
   Fpaintnow();
   return;
}


//  draw lines on image: trim rectangle, guidelines, gridlines

void trimrotate::drawlines(cairo_t *cr)
{
   using namespace trimrotate;
   
   Ntoplines = 4;                                                                //  outline trim rectangle
   toplines[0].x1 = trimx1;
   toplines[0].y1 = trimy1;
   toplines[0].x2 = trimx2;
   toplines[0].y2 = trimy1;
   toplines[0].type = 1;
   toplines[1].x1 = trimx2;
   toplines[1].y1 = trimy1;
   toplines[1].x2 = trimx2;
   toplines[1].y2 = trimy2;
   toplines[1].type = 1;
   toplines[2].x1 = trimx2;
   toplines[2].y1 = trimy2;
   toplines[2].x2 = trimx1;
   toplines[2].y2 = trimy2;
   toplines[2].type = 1;
   toplines[3].x1 = trimx1;
   toplines[3].y1 = trimy2;
   toplines[3].x2 = trimx1;
   toplines[3].y2 = trimy1;
   toplines[3].type = 1;

   if (Fguidelines) {                                                            //  vert/horz rotate guidelines
      Ntoplines = 6;
      toplines[4].x1 = guidelineX;
      toplines[4].y1 = 0;
      toplines[4].x2 = guidelineX;
      toplines[4].y2 = E3hh-1;
      toplines[4].type = 2;
      toplines[5].x1 = 0;
      toplines[5].y1 = guidelineY;
      toplines[5].x2 = E3ww-1;
      toplines[5].y2 = guidelineY;
      toplines[5].type = 2;
   }

   draw_toplines(1,cr);                                                          //  draw trim rectangle and guidelines
   
   return;
}


//  auto-trim image - set trim rectangle to exclude transparent regions

void trimrotate::autotrim()
{
   using namespace trimrotate;

   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
   pxm = E3pxm;
   if (pxm->nc < 4) return;                                                      //  no alpha channel

   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
   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] < 250) continue;                                         //    in transparent zones
            ppix = PXMpix(pxm,qx2,qy1);
            if (ppix[3] < 250) continue;
            ppix = PXMpix(pxm,qx2,qy2);
            if (ppix[3] < 250) continue;
            ppix = PXMpix(pxm,qx1,qy2);
            if (ppix[3] < 250) 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] < 250) break;
               ppix = PXMpix(pxm,qx,qy2);
               if (ppix[3] < 250) break;
            }
            if (qx < qx2) continue;

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

            Fgrow = 1;                                                           //  successfully grew the rectangle
            px1 = qx1;                                                           //  new bigger rectangle coordinates
            py1 = qy1;                                                           //    for the next search iteration
            px2 = qx2;
            py2 = qy2;
            goto breakout;                                                       //  leave all 4 loops
         }

      breakout:
         if (! Fgrow) break;
      }

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

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

   trimx1 = px1;                                                                 //  set parameters for trim function
   trimx2 = px2;
   trimy1 = py1;
   trimy2 = py2;

   trimww = px2 - px1;
   trimhh = py2 - py1;

   return;
}


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

void trimrotate::trim_customize()
{
   using namespace trimrotate;
   
   char        text[20], blab[8], rlab[8];
   float       r1, r2, ratio;
   cchar       *pp;
   int         ii, zstat;

/***
       ___________________________________________________________
      |          Trim Buttons                                     |
      |                                                           |
      |  label  [ 5:4 ] [ 4:3 ] [ 8:5 ] [ 16:9 ] [ 2:1 ] [ gold ] |
      |  ratio  [ 5:4 ] [ 4:3 ] [ 8:5 ] [ 16:9 ] [ 2:1 ] [1.62:1] |
      |                                                           |
      |                                           [Done] [Cancel] |
      |___________________________________________________________|
      
***/

   zdialog *zd = zdialog_new(E2X("Trim Buttons"),Mwin,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hbb","dialog",0,"homog|space=3");
   zdialog_add_widget(zd,"hbox","hbr","dialog",0,"homog|space=3");
   zdialog_add_widget(zd,"label","label","hbb",E2X("label"),"space=3");
   zdialog_add_widget(zd,"label","ratio","hbr",E2X("ratio"),"space=3");

   strcpy(blab,"butt-0");
   strcpy(rlab,"ratio-0");

   for (ii = 0; ii < 5; ii++)                                                    //  add current trimbuttons to dialog
   {
      blab[5] = '0' + ii;
      rlab[6] = '0' + ii;
      zdialog_add_widget(zd,"zentry",blab,"hbb",trimbuttons[ii],"size=4");
      zdialog_add_widget(zd,"zentry",rlab,"hbr",trimratios[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 < 5; ii++)                                                 //  get new button names
      {
         blab[5] = '0' + ii;
         zdialog_fetch(zd,blab,text,12);
         strTrim2(text);
         if (! *text) continue;
         zfree(trimbuttons[ii]);
         trimbuttons[ii] = zstrdup(text);
      }

      for (ii = 0; ii < 5; ii++)                                                 //  get new ratios
      {
         rlab[6] = '0' + ii;
         zdialog_fetch(zd,rlab,text,12);
         strTrim2(text);
         r1 = r2 = ratio = 0;
         pp = strField(text,':',1);
         if (! pp) continue;
         r1 = atof(pp);
         pp = strField(text,':',2);
         if (! pp) continue;
         r2 = atof(pp);
         if (r1 > 0 && r2 > 0) ratio = r1/r2;
         if (ratio < 0.1 || ratio > 10) continue;
         zfree(trimratios[ii]);
         trimratios[ii] = zstrdup(text);
      }
   }

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

   return;
}


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

//  Upright a rotated image: -90, +90, 180 degrees.
//  This is not an edit transaction: file is replaced and re-opened.
//  No loss of resolution.

void m_upright(GtkWidget *, cchar *menu)                                         //  20.0  auto-upright only
{
   int      angle = 0, nn;
   char     orientation = 0, *ppv[1];
   cchar    *exifkey[1] = { exif_orientation_key };
   cchar    *exifdata[1];
   cchar    *message = E2X("rotation unknown");

   F1_help_topic = "upright";
   if (checkpend("all")) return;

   if (clicked_file) {                                                           //  use clicked file if present
      if (! curr_file || ! strmatch(clicked_file,curr_file))                     //  bugfix  19.5
         f_open(clicked_file,0,0,1,0);                                           //  avoid f_open() re-entry
      clicked_file = 0;
   }

   if (! curr_file) return;                                                      //  use current file

   exif_get(curr_file,exifkey,ppv,1);                                            //  get EXIF: Orientation
   if (ppv[0]) {
      orientation = *ppv[0];                                                     //  single character
      zfree(ppv[0]);
   }
   if (orientation == '6') angle = +90;                                          //  rotate clockwise 90 deg.
   if (orientation == '8') angle = -90;                                          //  counterclockwise 90 deg.
   if (orientation == '3') angle = 180;                                          //  180 deg.
   if (! angle) {
      nn = zdialog_choose(Mwin,"mouse",message,"-90°","+90°","180°","quit",null);      //  20.0
      if (nn == 1) angle = -90;
      else if (nn == 2) angle = +90;
      else if (nn == 3) angle = 180;
      else return;
   }

   Fblock = 1;
   E0pxm = PXM_load(curr_file,1);                                                //  load poss. 16-bit image
   if (E0pxm) {
      E3pxm = PXM_rotate(E0pxm,angle);                                           //  rotate (threaded)
      PXM_free(E0pxm);
      E0pxm = E3pxm;
      E3pxm = 0;
   }

   f_save(curr_file,curr_file_type,curr_file_bpc,0,1);                           //  replace file

   exifdata[0] = (char *) "";                                                    //  remove EXIF orientation data
   exif_put(curr_file,exifkey,exifdata,1);                                       //  (f_save() omits if no edit made)

   update_image_index(curr_file);                                                //  update index data

   Fblock = 0;

   f_open(curr_file);
   gallery(curr_file,"paint",0);                                                 //  repaint gallery
   
   return;
}


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

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

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 active
   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 = saturated
   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 *, cchar *menu)
{
   using namespace retouch_names;

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

   GtkWidget   *drawwin_scale;

   F1_help_topic = "retouch";

   EFretouch.menuname = menu;
   EFretouch.menufunc = m_retouch;
   EFretouch.funcname = "retouch";
   EFretouch.FprevReq = 1;                                                       //  use preview
   EFretouch.Farea = 2;                                                          //  select area usable
   EFretouch.Frestart = 1;                                                       //  restart allowed
   EFretouch.FusePL = 1;                                                         //  use with paint/lever 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
      | |                                           | |                          //  1: curve for overall brightness
      | |             curve edit area               | |                          //  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 dark spot for black level          |                          //  click dark spot
      |  [x] Click gray spot for white balance        |                          //  click gray spot
      |  [_] Click for RGB distribution               |                          //  show RGB distribution in popup window
      |                                               |
      |  Brightness =============[]=================  |                          //  brightness
      |   Contrast  ===============[]===============  |                          //  contrast
      |  Saturation  =================[]============  |                          //  saturation B/W ... saturated
      |  Temperature ===============[]==============  |                          //  illumination temperature 
      |                                               |
      |  Settings File [Load] [Save]                  |
      |                                               |
      |                [Reset] [Prev] [Done] [Cancel] |
      |_______________________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Retouch"),Mwin,Breset,Bprev,Bdone,Bcancel,null);
   EFretouch.zd = zd;

   zdialog_add_widget(zd,"frame","frameH","dialog",0,"expand");                  //  edit-curve and distribution graph
   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",Ball,"space=5");
   zdialog_add_widget(zd,"radio","red","hbrgb",Bred,"space=3");
   zdialog_add_widget(zd,"radio","green","hbrgb",Bgreen,"space=3");
   zdialog_add_widget(zd,"radio","blue","hbrgb",Bblue,"space=3");

   zdialog_add_widget(zd,"hbox","hbabl","dialog");                               //  black level
   zdialog_add_widget(zd,"zbutton","autoBL","hbabl",E2X("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",E2X("sample %"));

   zdialog_add_widget(zd,"hbox","hbawb","dialog");                               //  white balance
   zdialog_add_widget(zd,"zbutton","autoWB","hbawb",E2X("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",E2X("sample %"));

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

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

   zdialog_add_widget(zd,"hbox","hbdist","dialog");                                //  click RGB distribution
   zdialog_add_widget(zd,"zbutton","dist","hbdist",E2X("Click for RGB distribution"),"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",E2X("Brightness"));
   zdialog_add_widget(zd,"label","labcont","vbcolor1",E2X("Contrast"));
   zdialog_add_widget(zd,"label","labsat1","vbcolor1",E2X("Saturation"));
   zdialog_add_widget(zd,"label","labtemp1","vbcolor1",E2X("Temperature"));

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

   zdialog_add_widget(zd,"hbox","hbset","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labset","hbset",E2X("Settings File"),"space=5");
   zdialog_add_widget(zd,"button","load","hbset",Bload,"space=3");
   zdialog_add_widget(zd,"button","save","hbset",Bsave,"space=3");

   zdialog_add_ttip(zd,Bprev,Bprevtip);

   zdialog_rescale(zd,"brightness",-1,0,1);                                      //  expand scale around neutral values
   zdialog_rescale(zd,"contrast",-1,0,1);
   zdialog_rescale(zd,"colorsat",-1,0,1);

   GtkWidget *frameH = zdialog_widget(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_widget(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, cchar *event)
{
   using namespace retouch_names;

   void  retouch_mousefunc();

   spldat      *sd = EFretouch.sd;    
   float       bright0, dbrite, cont0, dcont;
   float       dx, dy;
   int         ii;
   
   void  retouch_autoWB();
   void  retouch_autoBL();
   void  retouch_mousefunc();

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

   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();                                                           //  get full size image
      signal_thread();
      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;                                                                  //  19.0
   }

   if (zd->zstat == 2)                                                           //  [prev] restore previous settings
   {
      zd->zstat = 0;                                                             //  keep dialog active
      func_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)                                                           //  [done]
   {
      freeMouse();
      if (CEF->Fmods) {
         edit_fullsize();                                                        //  get full size image
         signal_thread();                                                        //  apply changes
         func_save_last_widgets(zd,sd,"retouch");                                //  save settings
         edit_done(0);                                                           //  complete edit
         whitebalance[0] = wbalR;                                                //  save settings for next time
         whitebalance[1] = wbalG;
         whitebalance[2] = wbalB;
         whitebalance[3] = colortemp;
      }
      else edit_cancel(0);
      return 1;
   }
   
   if (zd->zstat < 0 || zd->zstat > 3) {                                         //  [cancel] or [x]
      edit_cancel(0);
      return 1;
   }

   if (strmatch(event,"load"))                                                   //  load all settings from a file
      func_load_widgets(zd,sd,"retouch",null);

   if (strmatch(event,"save"))                                                   //  save all settings to a file
      func_save_widgets(zd,sd,"retouch",null);

   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,"dist")) m_RGB_dist(0,0);                                  //  popup RGB distribution 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 (zstrstr("brightness contrast colortemp colorsat "                         //  detect change
              "autoWB autoBL blendwidth 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

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


//  this function is called when a curve is edited

void retouch_curvedit(int spc)
{
   using namespace retouch_names;
   signal_thread();
   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         E1ww, E1hh;                                                       //  image dimensions
   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;
   
   E1ww = E1pxm->ww;                                                             //  get image dimensions
   E1hh = E1pxm->hh;

   for (py = 0; py < E1hh; py++)                                                 //  scan image file, get pixel counts
   for (px = 0; px < E1ww; px++)                                                 //    per RGB value 0-255
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E1pxm->ww + 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 = E1ww * E1hh;                                                     //  image pixel count
   if (sa_stat == 3) 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         E1ww, E1hh;                                                       //  image dimensions
   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;
   
   E1ww = E1pxm->ww;                                                             //  get image dimensions
   E1hh = E1pxm->hh;

   for (py = 0; py < E1hh; py++)                                                 //  scan image file, get pixel counts
   for (px = 0; px < E1ww; px++)                                                 //    per RGB value 0-255
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E1pxm->ww + 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 = E1ww * E1hh;                                                     //  image pixel count
   if (sa_stat == 3) 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;
   char        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 > E3pxm->ww-3) px = E3pxm->ww-3;
   if (py < 2) py = 2;
   if (py > E3pxm->hh-3) py = E3pxm->hh-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(mousetext,10,10,0,3);
   
   if (spotWB && 
      (red<5 || red>250 || green<5 || green>250 || blue<5 || blue>250)) {        //  refuse unscalable spot
      zmessageACK(Mwin,E2X("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;
      signal_thread();                                                           //  trigger image update
   }

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


//  thread function

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

   void * retouch_wthread(void *);

   while (true)
   {
      thread_idle_loop();                                                        //  wait for work or exit request
      
      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);
      }

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      do_wthreads(retouch_wthread,NWT);

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
      
      CEF->Fmods++;                                                              //  image3 modified
      CEF->Fsaved = 0;
   }

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


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

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

   for (py = index; py < E3pxm->hh; py += NWT)                                   //  loop all pixels
   for (px = 0; px < E3pxm->ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E3pxm->ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  pixel outside area
      }
      
      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

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

      //  apply RGB black points to input RGB values
      
      if (Fblack) {
         R3 = R3 - blackR;                                                       //  clip black value at low end 
         R3 = R3 * Rbscale;                                                      //  rescale so that high end = 256
         if (R3 < 0) R3 = 0;
         G3 = G3 - blackG;
         G3 = G3 * Gbscale;
         if (G3 < 0) G3 = 0;
         B3 = B3 - blackB;
         B3 = B3 * Bbscale;
         if (B3 < 0) B3 = 0;
      }

      //  apply white balance and temperature color shifts                       //  dampen as RGB values approach 256
      
      if (combR > 1) {
         F1 = (256 - R3) / 256;                                                  //  R1 = 0...256  >>  F1 = 1...0
         F2 = F1 * combR + 1 - F1;                                               //  F2 = combR ... 1
         R3 = F2 * R3;                                                           //  R3 is adjusted R1
      }
      else R3 = combR * R3;

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

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

      //  apply saturation color shift

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

      //  underflow/overflow test removed   v.20.0

      //  apply brightness/contrast curves

      if (sd->mod[0])                                                            //  curve was modified
      {
         pixbrite = R3;                                                          //  use max. RGB value
         if (G3 > pixbrite) pixbrite = G3;
         if (B3 > pixbrite) pixbrite = B3;
         
         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
         R3 = ff * R3;
         G3 = ff * G3;
         B3 = ff * B3;
      }

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

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

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

      if (R3 < 0) R3 = 0;                                                        //  stop underflow
      if (G3 < 0) G3 = 0;
      if (B3 < 0) B3 = 0;

      if (R3 > 255.9 || G3 > 255.9 || B3 > 255.9) {                              //  stop overflow
         maxRGB = R3;
         if (G3 > maxRGB) maxRGB = G3;
         if (B3 > maxRGB) maxRGB = B3;
         R3 = R3 * 255.9 / maxRGB;
         G3 = G3 * 255.9 / maxRGB;
         B3 = B3 * 255.9 / maxRGB;
      }

      //  select area edge blending

      if (sa_stat == 3 && dist < sa_blendwidth) {
         F2 = sa_blendfunc(dist);
         F1 = 1.0 - F2;
         R3 = F2 * R3 + F1 * R1;
         G3 = F2 * G3 + F1 * G1;
         B3 = F2 * B3 + F1 * B1;
      }

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

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


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

//  Resize (rescale) image

namespace resize_names
{
   editfunc  EFresize;
   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_resize(GtkWidget *, cchar *menu)
{
   using namespace resize_names;

   int    resize_dialog_event(zdialog *zd, cchar *event);
   void * resize_thread(void *);

   F1_help_topic = "resize";
   
   char     rtext[12];

   EFresize.menuname = menu;
   EFresize.menufunc = m_resize;
   EFresize.funcname = "resize";
   EFresize.Frestart = 1;                                                        //  allow restart
   EFresize.Fscript = 1;                                                         //  script supported                   20.0
   EFresize.threadfunc = resize_thread;                                          //  thread function
   if (! edit_setup(EFresize)) 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 [_]                    |
      |                                 [done] [cancel]  |
      |__________________________________________________|
    
****/

   zdialog *zd = zdialog_new(E2X("Resize Image"),Mwin,Bdone,Bcancel,null);
   EFresize.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",Bwidth);
   zdialog_add_widget(zd,"label","labh","vb11",Bheight);
   zdialog_add_widget(zd,"label","labpix","vb12","Pixels");
   zdialog_add_widget(zd,"zspin","width","vb12","20|30000|1|20");                //  fotoxx.h limits
   zdialog_add_widget(zd,"zspin","height","vb12","20|30000|1|20");
   zdialog_add_widget(zd,"label","labpct","vb13",Bpercent);
   zdialog_add_widget(zd,"zspin","pctwidth","vb13","1|400|1|100");
   zdialog_add_widget(zd,"zspin","pctheight","vb13","1|400|1|100");
   zdialog_add_widget(zd,"button","prev","vb14",E2X("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",E2X("W/H Ratio:"),"space=5");
   zdialog_add_widget(zd,"label","WHratio","hb4","1.6667");
   zdialog_add_widget(zd,"check","Flockratio","hb4",E2X("Lock"),"space=10");
   
   zdialog_add_ttip(zd,"prev",Bprevtip);

   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,resize_dialog_event,"save");                                   //  run dialog
   return;
}


//  dialog event and completion callback function

int resize_dialog_event(zdialog *zd, cchar *event)
{
   using namespace resize_names;

   int         nn;
   double      wwxhh, memreq, memavail;
   char        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                        20.0
      zdialog_fetch(zd,"width",width);
      zdialog_fetch(zd,"height",height);
      signal_thread();
      return 1;
   }
   
   if (zd->zstat)                                                                //  dialog complete
   {
      if (zd->zstat == 1) {                                                      //  done
         editresize[0] = width;                                                  //  remember size used
         editresize[1] = height;
         signal_thread();
         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 = editresize[0];                                                     //  set previous size
      height = editresize[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                              19.0
   }
   
   if (width < 20) {                                                             //  enforce lower limits
      width = 20;
      height = width / WHratio + 0.5;
   }

   if (height < 20) {
      height = 20;
      width = height * WHratio + 0.5;
   }
   
   if (width > wwhh_limit1) {                                                    //  enforce upper limits
      width = wwhh_limit1;
      height = width / WHratio + 0.5;
   }

   if (height > wwhh_limit1) {
      height = wwhh_limit1;
      width = height * WHratio + 0.5;
   }
      
   wwxhh = 1.0 * width * height;                                                 //  enforce image size limit
   if (wwxhh > wwhh_limit2) {
      width *= wwhh_limit2 / wwxhh;
      height *= wwhh_limit2 / wwxhh;
   }
   
   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          19.0
   memreq = memreq / 1024;                                                       //  peak required memory, KB
   parseprocfile("/proc/meminfo","MemAvailable:",&memavail,0);                   //  est. free memory (+ cache), KB
   memavail -= 200000;                                                           //  200 MB margin
   if (memreq > memavail) {
      zmessageACK(Mwin,E2X("insufficient memory, cannot proceed"));
      edit_cancel(0);
      return 1;
   }

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


//  do the resize job

void * resize_thread(void *)
{
   using namespace resize_names;

   PXM   *pxmtemp;

   while (true)
   {
      thread_idle_loop();                                                        //  wait for signal

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      pxmtemp = PXM_rescale(E1pxm,width,height);                                 //  rescale the edit image
      PXM_free(E3pxm);
      E3pxm = pxmtemp;

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

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();
   }

   return 0;
}


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

//  Adjust RGB menu function
//  Adjust Brightness, contrast, and color levels using RGB or CMY colors

namespace adjust_RGB_names
{
   editfunc    EF_RGB;                                                           //  edit function data
   float       RGB_inputs[8];
   int         E3ww, E3hh;
}


//  menu function

void m_adjust_RGB(GtkWidget *, cchar *menu)
{
   using namespace adjust_RGB_names;

   int    RGB_dialog_event(zdialog *zd, cchar *event);
   void * RGB_thread(void *);

   F1_help_topic = "adjust RGB";

   EF_RGB.menuname = menu;
   EF_RGB.menufunc = m_adjust_RGB;
   EF_RGB.funcname = "adjust_RGB";                                               //  function name
   EF_RGB.FprevReq = 1;                                                          //  use preview
   EF_RGB.Farea = 2;                                                             //  select area usable
   EF_RGB.Frestart = 1;                                                          //  allow restart
   EF_RGB.Fscript = 1;                                                           //  scripting supported
   EF_RGB.FusePL = 1;                                                            //  paint/lever edits supported
   EF_RGB.threadfunc = RGB_thread;                                               //  thread function
   if (! edit_setup(EF_RGB)) return;                                             //  setup edit
   
   E3ww = E3pxm->ww;
   E3hh = E3pxm->hh;

/***
    ________________________________
   |                                |
   |   +Brightness    =====[]=====  |
   |    +Red -Cyan    =====[]=====  |
   | +Green -Magenta  =====[]=====  |
   |   +Blue -Yellow  =====[]=====  |
   |                                |
   |     Contrast     =====[]=====  |
   |       Red        =====[]=====  |
   |      Green       =====[]=====  |
   |       Blue       =====[]=====  |
   |                                |
   |        [reset] [done] [cancel] |
   |________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Adjust RGB"),Mwin,Breset,Bdone,Bcancel,null); 
   EF_RGB.zd = zd;

   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb2",0,"homog");
   zdialog_add_widget(zd,"vbox","vb2","hb2",0,"homog|expand");
   zdialog_add_widget(zd,"label","labBriteDens","vb1",E2X("+Brightness"));
   zdialog_add_widget(zd,"label","labRedDens","vb1",E2X("+Red -Cyan"));
   zdialog_add_widget(zd,"label","labGreenDens","vb1",E2X("+Green -Magenta"));
   zdialog_add_widget(zd,"label","labBlueDens","vb1",E2X("+Blue -Yellow"));
   zdialog_add_widget(zd,"hsep","sep1","vb1");
   zdialog_add_widget(zd,"label","labContrast","vb1","Contrast All");
   zdialog_add_widget(zd,"label","labRedCon","vb1",E2X("Contrast Red"));
   zdialog_add_widget(zd,"label","labGreenCon","vb1",E2X("Contrast Green"));
   zdialog_add_widget(zd,"label","labBlueCon","vb1",E2X("Contrast Blue"));
   zdialog_add_widget(zd,"hscale","BriteDens","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale","RedDens","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale","GreenDens","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale","BlueDens","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hsep","sep2","vb2");
   zdialog_add_widget(zd,"hscale","Contrast","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale","RedCon","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale","GreenCon","vb2","-1|+1|0.001|0","expand");
   zdialog_add_widget(zd,"hscale","BlueCon","vb2","-1|+1|0.001|0","expand");

   zdialog_rescale(zd,"BriteDens",-1,0,+1);
   zdialog_rescale(zd,"RedDens",-1,0,+1);
   zdialog_rescale(zd,"GreenDens",-1,0,+1);
   zdialog_rescale(zd,"BlueDens",-1,0,+1);
   zdialog_rescale(zd,"Contrast",-1,0,+1);
   zdialog_rescale(zd,"RedCon",-1,0,+1);
   zdialog_rescale(zd,"GreenCon",-1,0,+1);
   zdialog_rescale(zd,"BlueCon",-1,0,+1);

   zdialog_resize(zd,300,0);
   zdialog_restore_inputs(zd);                                                   //  restore prior inputs
   zdialog_run(zd,RGB_dialog_event,"save");                                      //  run dialog - parallel
   
   zdialog_send_event(zd,"apply");
   return;
}


//  RGB dialog event and completion function

int RGB_dialog_event(zdialog *zd, cchar *event)                                  //  RGB dialog event function
{
   using namespace adjust_RGB_names;

   int      mod = 0;

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   
   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();
      E3ww = E3pxm->ww;
      E3hh = E3pxm->hh;
      signal_thread();
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  reset
         zd->zstat = 0;                                                          //  keep dialog active
         RGB_inputs[0] = 0;
         RGB_inputs[1] = 0;
         RGB_inputs[2] = 0;
         RGB_inputs[3] = 0;
         RGB_inputs[4] = 0;
         RGB_inputs[5] = 0;
         RGB_inputs[6] = 0;
         RGB_inputs[7] = 0;
         zdialog_stuff(zd,"BriteDens",0);
         zdialog_stuff(zd,"RedDens",0);
         zdialog_stuff(zd,"GreenDens",0);
         zdialog_stuff(zd,"BlueDens",0);
         zdialog_stuff(zd,"Contrast",0);
         zdialog_stuff(zd,"RedCon",0);
         zdialog_stuff(zd,"GreenCon",0);
         zdialog_stuff(zd,"BlueCon",0);
         edit_reset();
         return 1;                                                               //  19.0
      }
      else if (zd->zstat == 2) {                                                 //  done
         edit_fullsize();                                                        //  get full size image
         E3ww = E3pxm->ww;
         E3hh = E3pxm->hh;
         signal_thread();
         edit_done(0);                                                           //  commit edit
         return 1;
      }
      else {
         edit_cancel(0);                                                         //  discard edit
         return 1;
      }
   }

   if (strmatch("focus",event)) return 1;
   
   zdialog_fetch(zd,"BriteDens",RGB_inputs[0]);                                  //  get all inputs
   zdialog_fetch(zd,"RedDens",RGB_inputs[1]);
   zdialog_fetch(zd,"GreenDens",RGB_inputs[2]);
   zdialog_fetch(zd,"BlueDens",RGB_inputs[3]);
   zdialog_fetch(zd,"Contrast",RGB_inputs[4]);
   zdialog_fetch(zd,"RedCon",RGB_inputs[5]);
   zdialog_fetch(zd,"GreenCon",RGB_inputs[6]);
   zdialog_fetch(zd,"BlueCon",RGB_inputs[7]);

   if (RGB_inputs[0]) mod++;
   if (RGB_inputs[1]) mod++;
   if (RGB_inputs[2]) mod++;
   if (RGB_inputs[3]) mod++;
   if (RGB_inputs[4]) mod++;
   if (RGB_inputs[5]) mod++;
   if (RGB_inputs[6]) mod++;
   if (RGB_inputs[7]) mod++;
   
   if (mod) signal_thread();                                                     //  trigger update thread
   return 1;
}


//  thread function - multiple working threads to update image

void * RGB_thread(void *)
{
   using namespace adjust_RGB_names;

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

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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

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

      do_wthreads(RGB_wthread,NWT);                                              //  worker threads

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

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop warning
}


//  worker thread function

void * RGB_wthread(void *arg)                                                    //  overhauled
{
   using namespace adjust_RGB_names;

   float    R1, G1, B1, R3, G3, B3;
   float    briA, briR, briG, briB;
   float    conA, conR, conG, conB;
   float    R, G, B;

   int      index = *((int *) (arg));
   int      px, py, ii, dist = 0;
   float    *pix1, *pix3;
   float    cmax, F, f1, f2;

   briA = RGB_inputs[0];                                                         //  color brightness inputs, -1 to +1
   briR = RGB_inputs[1];
   briG = RGB_inputs[2];
   briB = RGB_inputs[3];

   R = briR - 0.5 * briG - 0.5 * briB + briA;                                    //  red = red - green - blue + all
   G = briG - 0.5 * briR - 0.5 * briB + briA;                                    //  etc.
   B = briB - 0.5 * briR - 0.5 * briG + briA;

   R += 1;                                                                       //  -1 ... 0 ... +1  >>  0 ... 1 ... 2
   G += 1;                                                                       //  increase the range
   B += 1;

   briR = R;                                                                     //  final color brightness factors
   briG = G;
   briB = B;

   conA = RGB_inputs[4];                                                         //  contrast inputs, -1 to +1
   conR = RGB_inputs[5];
   conG = RGB_inputs[6];
   conB = RGB_inputs[7];

   if (conA < 0) conA = 0.5 * conA + 1;                                          //  -1 ... 0  >>  0.5 ... 1.0
   else conA = conA + 1;                                                         //   0 ... 1  >>  1.0 ... 2.0
   if (conR < 0) conR = 0.5 * conR + 1;
   else conR = conR + 1;
   if (conG < 0) conG = 0.5 * conG + 1;
   else conG = conG + 1;
   if (conB < 0) conB = 0.5 * conB + 1;
   else conB = conB + 1;

   conR = conR * conA;                                                           //  apply overall contrast
   conG = conG * conA;
   conB = conB * conA;

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

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

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

      R3 = R1 * briR;                                                            //  apply color brightness factors
      G3 = G1 * briG;
      B3 = B1 * briB;

      R3 = conR * (R3 - 128) + 128;                                              //  apply contrast factors
      G3 = conG * (G3 - 128) + 128;
      B3 = conB * (B3 - 128) + 128;

      if (R3 < 0) R3 = 0;                                                        //  stop underflow
      if (G3 < 0) G3 = 0;
      if (B3 < 0) B3 = 0;

      if (R3 > 255.9 || G3 > 255.9 || B3 > 255.9) {                              //  stop overflow
         cmax = R3;
         if (G3 > cmax) cmax = G3;
         if (B3 > cmax) cmax = B3;
         F = 255.9 / cmax;
         R3 *= F;
         G3 *= F;
         B3 *= F;
      }

      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         f1 = sa_blendfunc(dist);                                                //    blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         R3 = f1 * R3 + f2 * R1;
         G3 = f1 * G3 + f2 * G1;
         B3 = f1 * B3 + f2 * B1;
      }

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

   pthread_exit(0);
}


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

//  HSL color menu function
//  Adjust colors using the HSL (hue/saturation/lightness) color model

namespace adjust_HSL_names
{
   GtkWidget   *RGBframe, *RGBcolor;
   GtkWidget   *Hframe, *Hscale;
   editfunc    EFHSL;                                                            //  edit function data
   int         E3ww, E3hh;
   int         Huse, Suse, Luse;                                                 //  "match using" flags, 0 or 1
   int         Hout, Sout, Lout;                                                 //  "output color" flags, 0 or 1
   float       Rm, Gm, Bm;                                                       //  RGB image color to match
   float       Hm, Sm, Lm;                                                       //  corresp. HSL color
   float       Mlev;                                                             //  match level 0..1 = 100%
   float       Hc, Sc, Lc;                                                       //  new color to add
   float       Rc, Gc, Bc;                                                       //  corresp. RGB color
   float       Adj;                                                              //  color adjustment, 0..1 = 100%
}


//  menu function

void m_adjust_HSL(GtkWidget *, cchar *menu)                                      //  overhauled
{
   using namespace adjust_HSL_names;

   void   HSL_RGBcolor(GtkWidget *drawarea, cairo_t *cr, int *);
   void   HSL_Hscale(GtkWidget *drawarea, cairo_t *cr, int *);
   int    HSL_dialog_event(zdialog *zd, cchar *event);
   void   HSL_mousefunc();
   void * HSL_thread(void *);
   
   F1_help_topic = "adjust HSL";

   EFHSL.menuname = menu;
   EFHSL.menufunc = m_adjust_HSL;
   EFHSL.funcname = "adjust_HSL";                                                //  function name
   EFHSL.FprevReq = 1;                                                           //  use preview
   EFHSL.Farea = 2;                                                              //  select area usable
   EFHSL.Frestart = 1;                                                           //  allow restart
   EFHSL.FusePL = 1;                                                             //  use with paint/lever edits OK
   EFHSL.mousefunc = HSL_mousefunc;                                              //  mouse function
   EFHSL.threadfunc = HSL_thread;                                                //  thread function
   if (! edit_setup(EFHSL)) return;                                              //  setup edit
   
   E3ww = E3pxm->ww;
   E3hh = E3pxm->hh;

/***
       ___________________________________________________
      |                                                   |
      |  Input color to match and adjust: [#####]         |
      |  Match using: [] Hue  [] Saturation  [] Lightness |
      |  Match Level: ==================[]========== 100% |
      |  - - - - - - - - - - - - - - - - - - - - - - - -  |
      |  Output Color                                     |
      |  [########]  [#################################]  |                      //  new color and hue spectrum
      |  [] Color Hue   ================[]==============  |
      |  [] Saturation  =====================[]=========  |
      |  [] Lightness   ===========[]===================  |
      |  Adjustment  ===================[]========== 100% |
      |                                                   |
      |                          [reset] [done] [cancel]  |
      |___________________________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Adjust HSL"),Mwin,Breset,Bdone,Bcancel,null);
   EFHSL.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"label","labmatch","hb1",E2X("Input color to match and adjust:"),"space=5");
   zdialog_add_widget(zd,"colorbutt","matchRGB","hb1","0|0|0");
   zdialog_add_ttip(zd,"matchRGB",E2X("shift+click on image to select color"));
   
   zdialog_add_widget(zd,"hbox","hbmu","dialog");
   zdialog_add_widget(zd,"label","labmu","hbmu",E2X("Match using:"),"space=5");
   zdialog_add_widget(zd,"check","Huse","hbmu",E2X("Hue"),"space=3");
   zdialog_add_widget(zd,"check","Suse","hbmu",E2X("Saturation"),"space=3");
   zdialog_add_widget(zd,"check","Luse","hbmu",E2X("Lightness"),"space=3");
   
   zdialog_add_widget(zd,"hbox","hbmatch","dialog");
   zdialog_add_widget(zd,"label","labmatch","hbmatch",Bmatchlevel,"space=5");
   zdialog_add_widget(zd,"hscale","Mlev","hbmatch","0|1|0.001|1.0","expand");
   zdialog_add_widget(zd,"label","lab100%","hbmatch","100%","space=4");

   zdialog_add_widget(zd,"hsep","sep","dialog",0,"space=5");
   
   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"label","laboutput","hb1",E2X("Output Color"));

   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb2",0,"homog");
   zdialog_add_widget(zd,"vbox","vb2","hb2",0,"homog|expand");

   zdialog_add_widget(zd,"frame","RGBframe","vb1",0,"space=1");                  //  drawing area for RGB color
   RGBframe = zdialog_widget(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_RGBcolor,0);

   zdialog_add_widget(zd,"frame","Hframe","vb2",0,"space=1");                    //  drawing area for hue scale
   Hframe = zdialog_widget(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_Hscale,0);

   zdialog_add_widget(zd,"check","Hout","vb1",E2X("Color Hue"));
   zdialog_add_widget(zd,"check","Sout","vb1",E2X("Saturation"));
   zdialog_add_widget(zd,"check","Lout","vb1",E2X("Lightness"));
   zdialog_add_widget(zd,"label","labadjust","vb1",E2X("Adjustment"));

   zdialog_add_widget(zd,"hscale","Hc","vb2","0|359.9|0.1|180","expand");
   zdialog_add_widget(zd,"hscale","Sc","vb2","0|1|0.001|0.5","expand");
   zdialog_add_widget(zd,"hscale","Lc","vb2","0|1|0.001|0.5","expand");
   zdialog_add_widget(zd,"hbox","vb2hb","vb2");
   zdialog_add_widget(zd,"hscale","Adj","vb2hb","0|1|0.001|0.0","expand");
   zdialog_add_widget(zd,"label","lab100%","vb2hb","100%","space=4");

   zdialog_stuff(zd,"Huse",1);                                                   //  default: match on hue and saturation
   zdialog_stuff(zd,"Suse",1);
   zdialog_stuff(zd,"Luse",0);
   zdialog_stuff(zd,"Hout",1);                                                   //  default: replace only hue
   zdialog_stuff(zd,"Sout",0);
   zdialog_stuff(zd,"Lout",0);

   Rm = Gm = Bm = 0;                                                             //  color to match = black = not set
   Hm = Sm = Lm = 0;
   Huse = Suse = 1;
   Luse = 0;
   Hout = 1;
   Sout = Lout = 0;
   Mlev = 1.0;
   Hc = 180;                                                                     //  new HSL color to set / mix
   Sc = 0.5;
   Lc = 0.5;
   Adj = 0.0;
   
   zdialog_run(zd,HSL_dialog_event,"save");                                      //  run dialog - parallel
   takeMouse(HSL_mousefunc,arrowcursor);                                         //  connect mouse function
   return;
}


//  Paint RGBcolor drawing area with RGB color from new HSL color

void HSL_RGBcolor(GtkWidget *drawarea, cairo_t *cr, int *)
{
   using namespace adjust_HSL_names;

   int      ww, hh;

   ww = gtk_widget_get_allocated_width(drawarea);                                //  drawing area size
   hh = gtk_widget_get_allocated_height(drawarea);
   
   HSLtoRGB(Hc,Sc,Lc,Rc,Gc,Bc);                                                  //  new RGB color

   cairo_set_source_rgb(cr,Rc,Gc,Bc);
   cairo_rectangle(cr,0,0,ww-1,hh-1);
   cairo_fill(cr);

   return;
}


//  Paint Hscale drawing area with all hue values in a horizontal scale

void HSL_Hscale(GtkWidget *drawarea, cairo_t *cr, int *)
{
   using namespace adjust_HSL_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_dialog_event(zdialog *zd, cchar *event)                                  //  HSL dialog event function
{
   using namespace adjust_HSL_names;

   void   HSL_mousefunc();

   int      mod = 0;

   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   
   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();
      E3ww = E3pxm->ww;
      E3hh = E3pxm->hh;
      signal_thread();
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  reset
         zd->zstat = 0;                                                          //  keep dialog active
         Mlev = 1.0;
         Hc = 180;                                                               //  set defaults
         Sc = 0.5;
         Lc = 0.5;
         Adj = 0.0;
         zdialog_stuff(zd,"Mlev",Mlev);
         zdialog_stuff(zd,"Hc",Hc);
         zdialog_stuff(zd,"Sc",Sc);
         zdialog_stuff(zd,"Lc",Lc);
         zdialog_stuff(zd,"Adj",Adj);
         edit_reset();
         return 1;                                                               //  19.0
      }
      else if (zd->zstat == 2) {                                                 //  done
         edit_fullsize();                                                        //  get full size image
         E3ww = E3pxm->ww;
         E3hh = E3pxm->hh;
         signal_thread();
         edit_done(0);                                                           //  commit edit
         return 1;
      }
      else {
         edit_cancel(0);                                                         //  discard edit
         return 1;
      }
   }

   if (strmatch("focus",event)) {
      takeMouse(HSL_mousefunc,arrowcursor);
      return 1;
   }
   
   if (strmatch(event,"Huse")) {                                                 //  match on Hue, 0/1
      zdialog_fetch(zd,"Huse",Huse);
      mod = 1;
   }
   
   if (strmatch(event,"Suse")) {                                                 //  match on Saturation, 0/1
      zdialog_fetch(zd,"Suse",Suse);
      mod = 1;
   }
   
   if (strmatch(event,"Luse")) {                                                 //  match on Lightness, 0/1
      zdialog_fetch(zd,"Luse",Luse);
      mod = 1;
   }
   
   if (strmatch(event,"Hout")) {                                                 //  replace Hue, 0/1
      zdialog_fetch(zd,"Hout",Hout);
      mod = 1;
   }
   
   if (strmatch(event,"Sout")) {                                                 //  replace Saturation, 0/1
      zdialog_fetch(zd,"Sout",Sout);
      mod = 1;
   }
   
   if (strmatch(event,"Lout")) {                                                 //  replace Lightness, 0/1
      zdialog_fetch(zd,"Lout",Lout);
      mod = 1;
   }
   
   if (strmatch("Mlev",event)) {                                                 //  color match 0..1 = 100%
      zdialog_fetch(zd,"Mlev",Mlev);
      mod = 1;
   }

   if (strmatch("Hc",event)) {                                                   //  new color hue 0-360
      zdialog_fetch(zd,"Hc",Hc);
      mod = 1;
   }
      
   if (strmatch("Sc",event)) {                                                   //  saturation 0-1
      zdialog_fetch(zd,"Sc",Sc);
      mod = 1;
   }

   if (strmatch("Lc",event)) {                                                   //  lightness 0-1
      zdialog_fetch(zd,"Lc",Lc);
      mod = 1;
   }

   if (strmatch("Adj",event)) {                                                  //  adjustment 0..1 = 100%
      zdialog_fetch(zd,"Adj",Adj);
      mod = 1;
   }
   
   if (strmatch("blendwidth",event)) mod = 1;                                    //  area blend width changed
   
   if (mod) {
      gtk_widget_queue_draw(RGBcolor);                                           //  draw current RGB color
      signal_thread();                                                           //  trigger update thread
   }

   return 1;
}


//  mouse function
//  click on image to set the color to match and change

void HSL_mousefunc()
{
   using namespace adjust_HSL_names;

   int         mx, my, px, py;
   char        color[20];
   float       *pix1, R, G, B;
   float       f256 = 1.0 / 256.0;
   zdialog     *zd = EFHSL.zd;

   if (! KBshiftkey) return;                                                     //  check shift + left or right click
   if (! LMclick && ! RMclick) return;

   mx = Mxclick;                                                                 //  clicked pixel on image
   my = Myclick;

   if (mx < 1) mx = 1;                                                           //  pull back from image edge
   if (mx > E3ww - 2) mx = E3ww - 2;
   if (my < 1) my = 1;
   if (my > E3hh - 2) my = E3hh - 2;
   
   R = G = B = 0;

   for (py = my-1; py <= my+1; py++)                                             //  compute average RGB for 3x3
   for (px = mx-1; px <= mx+1; px++)                                             //    block of pixels
   {
      pix1 = PXMpix(E1pxm,px,py);
      R += pix1[0];
      G += pix1[1];
      B += pix1[2];
   }
   
   R = R / 9;
   G = G / 9;
   B = B / 9;

   if (LMclick)                                                                  //  left mouse click
   {                                                                             //  pick MATCH color from image
      LMclick = 0;
      snprintf(color,19,"%.0f|%.0f|%.0f",R,G,B);                                 //  draw new match color button
      if (zd) zdialog_stuff(zd,"matchRGB",color);
      Rm = R * f256;
      Gm = G * f256;
      Bm = B * f256;
      RGBtoHSL(Rm,Gm,Bm,Hm,Sm,Lm);                                               //  set HSL color to match
      signal_thread();                                                           //  trigger update thread              20.0
   }
   
   if (RMclick)                                                                  //  right mouse click
   {                                                                             //  pick OUTPUT color from image
      RMclick = 0;
      R = R * f256;
      G = G * f256;
      B = B * f256;
      RGBtoHSL(R,G,B,Hc,Sc,Lc);                                                  //  output HSL
      zdialog_stuff(zd,"Hc",Hc);
      zdialog_stuff(zd,"Sc",Sc);
      zdialog_stuff(zd,"Lc",Lc);
      gtk_widget_queue_draw(RGBcolor);                                           //  draw current RGB color
      signal_thread();                                                           //  trigger update thread
   }

   return;
}


//  thread function - multiple working threads to update image

void * HSL_thread(void *)
{
   using namespace adjust_HSL_names;

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

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

      while (Fpaintrequest) zmainsleep(0.01);
      paintlock(1);                                                              //  block window paint                 19.0

      do_wthreads(HSL_wthread,NWT);                                              //  worker threads

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

      paintlock(0);                                                              //  unblock window paint               19.0
      Fpaint2();                                                                 //  update window
   }

   return 0;                                                                     //  not executed, stop warning
}


//  worker thread function

void * HSL_wthread(void *arg)
{
   using namespace adjust_HSL_names;
   
   int      index = *((int *) (arg));
   int      px, py, ii, dist = 0;
   float    *pix1, *pix3;
   float    R1, G1, B1, R3, G3, B3;
   float    H1, S1, L1, H3, S3, L3;
   float    dH, dS, dL, match;
   float    a1, a2, f1, f2;
   float    f256 = 1.0 / 256.0;

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

      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      
      R1 = f256 * pix1[0];                                                       //  input pixel RGB
      G1 = f256 * pix1[1];
      B1 = f256 * pix1[2];
      
      RGBtoHSL(R1,G1,B1,H1,S1,L1);                                               //  convert to HSL

      match = 1.0;                                                               //  compare image pixel to match HSL

      if (Huse) {
         dH = fabsf(Hm - H1);
         if (360 - dH < dH) dH = 360 - dH;
         dH *= 0.002778;                                                         //  H difference, normalized 0..1
         match = 1.0 - dH;
      }

      if (Suse) {
         dS = fabsf(Sm - S1);                                                    //  S difference, 0..1
         match *= (1.0 - dS);
      }

      if (Luse) {
         dL = fabsf(Lm - L1);                                                    //  L difference, 0..1
         match *= (1.0 - dL);
      }

      a1 = pow(match, 10.0 * Mlev);                                              //  color selectivity, 0..1 = max
      a1 = Adj * a1;
      a2 = 1.0 - a1;
      
      if (Hout) H3 = a1 * Hc + a2 * H1;                                          //  output HSL = a1 * new HSL
      else H3 = H1;                                                              //             + a2 * old HSL
      if (Sout) S3 = a1 * Sc + a2 * S1;
      else S3 = S1;
      if (Lout) L3 = a1 * Lc + a2 * L1;
      else L3 = L1;
      
      HSLtoRGB(H3,S3,L3,R3,G3,B3);
      
      if (sa_stat == 3 && dist < sa_blendwidth) {                                //  select area is active,
         f1 = sa_blendfunc(dist);                                                //    blend changes over sa_blendwidth
         f2 = 1.0 - f1;
         R3 = f1 * R3 + f2 * R1;
         G3 = f1 * G3 + f2 * G1;
         B3 = f1 * B3 + f2 * B1;
      }
      
      pix3[0] = 255.0 * R3;
      pix3[1] = 255.0 * G3;
      pix3[2] = 255.0 * B3;
   }

   pthread_exit(0);
}


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


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

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

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

   zdialog  *zd;

   F1_help_topic = "markup";

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

***/

   zd = zdialog_new(E2X("Image Markup"),Mwin,Bcancel,0);
   zdialog_add_widget(zd,"check","text","dialog",E2X("Draw text on image"));
   zdialog_add_widget(zd,"check","line","dialog",E2X("Draw line or arrow on image"));
   zdialog_add_widget(zd,"check","box","dialog",E2X("Draw box on image"));
   zdialog_add_widget(zd,"check","oval","dialog",E2X("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, cchar *event)
{
   if (zd->zstat) {
      zdialog_free(zd);
      return 1;
   }
   
   if (! zstrstr("text line box oval",event)) return 1;
   zdialog_free(zd);

   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);
   
   return 1; 
}


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

//  add text on top of the image

namespace drawtext_names
{
   #define Bversion E2X("+Version")

   textattr_t  attr;                                                             //  text attributes and image

   char     file[1000] = "";                                                     //  file for write_text data
   char     metakey[60] = "";
   int      px, py;                                                              //  text position on image
   int      textpresent;                                                         //  flag, text present on image

   int   dialog_event(zdialog *zd, cchar *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 *, cchar *menu)
{
   using namespace drawtext_names;

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

   F1_help_topic = "draw text";                                                  //  user guide topic

   EFdrawtext.menufunc = m_draw_text;
   EFdrawtext.funcname = "draw_text";
   EFdrawtext.Farea = 1;                                                         //  select area ignored
   EFdrawtext.Frestart = 1;                                                      //  allow restart
   EFdrawtext.mousefunc = mousefunc;
   if (! edit_setup(EFdrawtext)) return;                                         //  setup edit

/***
       ____________________________________________________________________
      |                     Draw text on image                             |
      |                                                                    |
      |  Enter text, click/drag on image, right click to remove.           |
      |                                                                    |
      |  Use settings file  [Open] [Save]                                  |     Bopen Bsave
      |  Text [____________________________________________________]       |     text
      |  Use metadata key [________________________________] [Fetch]       |     metakey Bfetch
      |  [Font] [FreeSans_________]  Size [ 44|v]                          |     Bfont fontname fontsize
      |                                                                    |
      |            color   transp.   width     angle                       |
      |  text     [#####] [_______]           [______]                     |     txcolor txtransp txangle
      |  backing  [#####] [_______]                                        |     bgcolor bgtransp
      |  outline  [#####] [_______] [_______]                              |     tocolor totransp towidth
      |  shadow   [#####] [_______] [_______] [______]                     |     shcolor shtransp shwidth shangle
      |                                                                    |
      |        [Clear] [Replace] [+Version] [Next] [Apply] [Done] [Cancel] |
      |____________________________________________________________________|

      [Clear]     clear text and metadata key
      [Replace]   edit_done(), replace current file, restart dialog
      [+Version]  edit_done(), create new file version, restart dialog
      [Next]      edit_done(), replace current file, move to next file, restart dialog, write same text
      [Apply]     edit_done, restart dialog
      [Done]      edit_done()
      [Cancel]    edit_cancel()

***/

   zdialog *zd = zdialog_new(title,Mwin,Bclear,Breplace,Bversion,Bnext,Bapply,Bdone,Bcancel,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","hbfile","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labfile","hbfile",E2X("Use settings file"),"space=3");
   zdialog_add_widget(zd,"button",Bopen,"hbfile",Bopen);
   zdialog_add_widget(zd,"button",Bsave,"hbfile",Bsave);

   zdialog_add_widget(zd,"hbox","hbtext","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labtext","hbtext",E2X("Text"),"space=5");
   zdialog_add_widget(zd,"frame","frtext","hbtext",0,"expand");
   zdialog_add_widget(zd,"edit","text","frtext","text","expand|wrap");
   
   zdialog_add_widget(zd,"hbox","hbmeta","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labmeta","hbmeta",E2X("Use metadata key"),"space=5");
   zdialog_add_widget(zd,"zentry","metakey","hbmeta",0,"space=2|expand");
   zdialog_add_widget(zd,"button",Bfetch,"hbmeta",Bfetch);

   zdialog_add_widget(zd,"hbox","hbfont","dialog",0,"space=2");
   zdialog_add_widget(zd,"button",Bfont,"hbfont",Bfont);
   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",Bsize);
   zdialog_add_widget(zd,"zspin","fontsize","hbfont","8|500|1|40","space=3");

   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=2");
   zdialog_add_widget(zd,"vbox","vbattr3","hbattr",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vbattr4","hbattr",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vbattr5","hbattr",0,"homog|space=2");

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

   zdialog_add_widget(zd,"label","labcol","vbattr2",Bcolor);
   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","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",Bwidth);
   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",Bangle);
   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_ttip(zd,Breplace,E2X("save to current file"));
   zdialog_add_ttip(zd,Bversion,E2X("save as new file version"));
   zdialog_add_ttip(zd,Bnext,E2X("save to current file \n"
                                 "open next file with same text"));

   zdialog_restore_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,"metakey",metakey,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 (*metakey) zdialog_send_event(zd,Bfetch);                                  //  metadata key active, get text
   return;
}


//  dialog event and completion callback function

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

   GtkWidget   *font_dialog;
   char        font[100];                                                        //  font name and size
   int         size, err;
   char        *newfilename, *pp;
   cchar       *keyname[1];
   char        *keyvals[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 < 0 || zd->zstat > 7) zd->zstat = 7;                         //  cancel

      if (zd->zstat == 1) {                                                      //  clear all inputs
         *attr.text = 0;
         *metakey = 0;
         zdialog_stuff(zd,"text","");
         zdialog_stuff(zd,"metakey","");
         zd->zstat = 0;                                                          //  keep dialog active
         write(2);                                                               //  erase text on image         bugfix 19.0
         return 1;
      }
   
      if (zd->zstat == 2) {                                                      //  replace current file
         if (textpresent) edit_done(0);                                          //  finish
         else edit_cancel(0);
         f_save(curr_file,curr_file_type,curr_file_bpc,0,1);                     //  replace curr. file
         curr_file_size = f_save_size;
         m_draw_text(0,0);                                                       //  start again
         return 1;
      }

      if (zd->zstat == 3) {                                                      //  make a new file version
         if (textpresent) edit_done(0);                                          //  finish
         else edit_cancel(0);
         newfilename = file_new_version(curr_file);                              //  get next avail. file version name
         if (! newfilename) return 1;
         err = f_save(newfilename,curr_file_type,curr_file_bpc,0,1);             //  save file
         if (! err) f_open_saved();                                              //  open saved file with edit hist
         zfree(newfilename);
         m_draw_text(0,0);                                                       //  start again
         return 1;
      }

      if (zd->zstat == 4) {                                                      //  finish and go to next image file
         if (textpresent) edit_done(0);                                          //  save mods
         else edit_cancel(0);
         f_save(curr_file,curr_file_type,curr_file_bpc,0,1);                     //  replace curr. file
         curr_file_size = f_save_size;
         m_next(0,0);                                                            //  open next file
         m_draw_text(0,0);                                                       //  start again
         write(1);                                                               //  put same text etc. onto image
         return 1;
      }
      
      if (zd->zstat == 5) {                                                      //  apply                              20.0
         zd->zstat = 0;
         if (! textpresent) return 1;
         edit_apply();
         px = py = -1;
         return 1;
      }

      if (zd->zstat == 6) {                                                      //  done
         if (textpresent) edit_done(0);                                          //  save mods
         else edit_cancel(0);
         return 1;
      }

      if (zd->zstat == 7) {
         edit_cancel(0);                                                         //  cancel or [x]
         return 1;
      }
   }

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

   if (strmatch(event,Bopen))                                                    //  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);
   }

   if (strmatch(event,Bsave)) {                                                  //  save zdialog fields to file
      save_text(zd);
      return 1;
   }
   
   if (strmatch(event,Bfetch)) {                                                 //  load text from metadata keyname
      zdialog_fetch(zd,"metakey",metakey,60);
      if (*metakey < ' ') return 1;
      keyname[0] = metakey;
      exif_get(curr_file,keyname,keyvals,1);
      if (! keyvals[0]) return 1;
      if (strlen(keyvals[0]) > 999) keyvals[0][999] = 0;
      repl_1str(keyvals[0],attr.text,"\\n","\n");                                //  replace "\n" with newlines
      zfree(keyvals[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,Bfont)) {                                                  //  select new font
      snprintf(font,100,"%s %d",attr.font,attr.size);
      font_dialog = gtk_font_chooser_dialog_new(E2X("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);

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

   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
      px = Mxclick;                                                              //  new text position on image
      py = Myclick;
      write(1);                                                                  //  erase old, write new text on image
   }

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

   if (Mxdrag || Mydrag)                                                         //  mouse dragged
   {
      px = Mxdrag;                                                               //  new text position on image
      py = 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   20.0

   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);
   
   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 >= E3pxm->ww) continue;
         if (py3 < 0 || py3 >= E3pxm->hh) 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 (px < 0 && py < 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 (px > E3pxm->ww) px = E3pxm->ww;                                           //  if off screen, pull back in sight
   if (py > E3pxm->hh) py = E3pxm->hh;

   orgx2 = px - ww2/2;                                                           //  copy-to image3 location
   orgy2 = py - hh2/2;

   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 >= E3pxm->ww) continue;                                 //  omit parts beyond edges
      if (py3 < 0 || py3 >= E3pxm->hh) 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;
      }
   }

   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 from a file

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

   file = zgetfile(dialogtitle,MWIN,"file",drawtext_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,"\\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 = (char *) 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,E2X("text file is defective"));
   printz("buff: %s\n",buff);
}


//  save text to a file

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

   file = zgetfile(dialogtitle,MWIN,"save",drawtext_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,"\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
      char     text[1000];                               //  text to generate image from
      char     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
      char     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;                                //  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;
   char           font[100];                                                     //  font name and size
   int            red, green, blue;
   float          txpart, topart, shpart, bgpart;

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

   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(strField(attr->color[0],'|',1));                                 //  get text foreground color
   txgreen = atoi(strField(attr->color[0],'|',2));
   txblue = atoi(strField(attr->color[0],'|',3));
   bgred = atoi(strField(attr->color[1],'|',1));                                 //  get text background color
   bggreen = atoi(strField(attr->color[1],'|',2));
   bgblue = atoi(strField(attr->color[1],'|',3));
   tored = atoi(strField(attr->color[2],'|',1));                                 //  get text outline color
   togreen = atoi(strField(attr->color[2],'|',2));
   toblue = atoi(strField(attr->color[2],'|',3));
   shred = atoi(strField(attr->color[3],'|',1));                                 //  get text shadow color
   shgreen = atoi(strField(attr->color[3],'|',2));
   shblue = atoi(strField(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, cchar *event);                                //  dialog event function
   void  mousefunc();                                                            //  mouse event function
   void  write(int mode);                                                        //  write line on image

   editfunc    EFdrawline;
}


void m_draw_line(GtkWidget *, cchar *menu)
{
   using namespace drawline_names;

   cchar    *intro = E2X("Enter line or arrow properties in dialog, \n"
                         "click/drag on image, right click to remove");

   F1_help_topic = "draw line";                                                  //  user guide topic

   EFdrawline.menufunc = m_draw_line;
   EFdrawline.funcname = "draw_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
      |                                                   |
      |                           [Apply] [Done] [Cancel] |
      |___________________________________________________|

***/

   zd = zdialog_new(E2X("Draw line or arrow on image"),Mwin,Bapply,Bdone,Bcancel,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",E2X("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",Bwidth,"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",E2X("Arrow head"),"space=5");
   zdialog_add_widget(zd,"check","larrow","hbarrow",Bleft);
   zdialog_add_widget(zd,"label","space","hbarrow",0,"space=10");
   zdialog_add_widget(zd,"check","rarrow","hbarrow",Bright);

   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=2");
   zdialog_add_widget(zd,"vbox","vbcol3","hbcol",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vbcol4","hbcol",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vbcol5","hbcol",0,"homog|space=2");

   zdialog_add_widget(zd,"label","space","vbcol1");
   zdialog_add_widget(zd,"label","labline","vbcol1",E2X("line"));
   zdialog_add_widget(zd,"label","labback","vbcol1",E2X("backing"));
   zdialog_add_widget(zd,"label","laboutln","vbcol1",E2X("outline"));
   zdialog_add_widget(zd,"label","labshadow","vbcol1",E2X("shadow"));

   zdialog_add_widget(zd,"label","labcol","vbcol2",Bcolor);
   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","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",Bwidth);
   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",Bangle);
   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,Bapply,E2X("fix line/arrow in layout \n start new line/arrow"));

   zdialog_restore_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, cchar *event)
{
   using namespace drawline_names;

   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) {                                                      //  Apply, commit present line to image
         zd->zstat = 0;
         write(1);
         linepresent = 0;                                                        //  (no old line to erase)
         mpx = mpy = -1;
         edit_apply();
         return 1;
      }

      if (zd->zstat == 2 && CEF->Fmods) edit_done(0);                            //  Done, complete pending edit
      else edit_cancel(0);                                                       //  Cancel or kill
      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
   zmainloop();
   return 1;
}


//  mouse function, set new position for line on image

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

   float    ax1, ay1, ax2, ay2;                                                  //  line/arrow end points
   float    angle, rad, l2;
   float    amx, amy;
   float    d1, d2, 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;
   }

   //  move the closest line endpoint to the mouse position and leave the other endpoint fixed

   angle = attr.angle;
   rad = -angle / 57.296;
   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;
   
   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);

   d1 = sqrtf(d1);                                                               //  mouse - end point distance
   d2 = sqrtf(d2);

   if (d1 < d2) {                                                                //  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 {                                                                        //  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;
   }

   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 >= E3pxm->ww) continue;
         if (py3 < 0 || py3 >= E3pxm->hh) 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 >= E3pxm->ww) continue;                                 //  omit parts beyond edges
      if (py3 < 0 || py3 >= E3pxm->hh) 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(strField(attr->color[0],'|',1));                                 //  get line foreground color
   lngreen = atoi(strField(attr->color[0],'|',2));
   lnblue = atoi(strField(attr->color[0],'|',3));
   bgred = atoi(strField(attr->color[1],'|',1));                                 //  get line background color
   bggreen = atoi(strField(attr->color[1],'|',2));
   bgblue = atoi(strField(attr->color[1],'|',3));
   tored = atoi(strField(attr->color[2],'|',1));                                 //  get line outline color
   togreen = atoi(strField(attr->color[2],'|',2));
   toblue = atoi(strField(attr->color[2],'|',3));
   shred = atoi(strField(attr->color[3],'|',1));                                 //  get line shadow color
   shgreen = atoi(strField(attr->color[3],'|',2));
   shblue = atoi(strField(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 *, cchar *)                                            //  20.0
{
   using namespace drawbox_names;

   int drawbox_dialog_event(zdialog *zd, cchar *event);
   void drawbox_mousefunc();
   
   zdialog     *zd;
   cchar       *tip = E2X("drag mouse to draw box \n"
                          "shift + drag center to move box \n"
                          "shift + drag edge to move edge");
   
   F1_help_topic = "draw box";

   EFdrawbox.menufunc = m_draw_box;
   EFdrawbox.funcname = "draw_box";
   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 [__] |
         |                                   |
         |           [apply] [done] [cancel] |
         |___________________________________|

***/

   zd = zdialog_new(E2X("Draw box on image"),Mwin,Bapply,Bdone,Bcancel,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",E2X("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",E2X("line width"),"space=3");
   zdialog_add_widget(zd,"zspin","width","hbline","1|10|1|1","space=3");
   
   zdialog_restore_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, cchar *event)
{
   using namespace drawbox_names;

   char     color[20];
   cchar    *pp;

   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) {                                                      //  apply
         zd->zstat = 0;
         edit_apply(); 
         return 1;
      }
   
      if (zd->zstat == 2) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }

   if (zstrstr("init RGB width",event))                                          //  set new line color and width
   {
      zdialog_fetch(zd,"RGB",color,19);
      pp = strField(color,"|",1);
      if (pp) RGB[0] = atoi(pp);
      pp = strField(color,"|",2);
      if (pp) RGB[1] = atoi(pp);
      pp = strField(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_free(E3pxm);                                                           //  erase prior
      E3pxm = PXM_copy(E1pxm);
      
      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 = oval/circle
   int      mx1, my1, mx2, my2;                                                  //  mouse drag coordinates
   float    exa, exb;                                                            //  ellipse axes
}


//  menu function

void m_draw_oval(GtkWidget *, cchar *)                                           //  20.0
{
   using namespace drawoval_names;

   int drawoval_dialog_event(zdialog *zd, cchar *event);
   void drawoval_mousefunc();
   
   zdialog     *zd;
   cchar       *tip = E2X("drag mouse down/right to draw oval \n"
                          "shift + drag center to move oval \n"
                          "shift + drag lower right edge to change");
   
   F1_help_topic = "draw oval";

   EFdrawoval.menufunc = m_draw_oval;
   EFdrawoval.funcname = "draw_oval";
   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] oval  [x] circle                    |
         |                                         |
         |                 [apply] [done] [cancel] |
         |_________________________________________|

***/

   zd = zdialog_new(E2X("Draw oval on image"),Mwin,Bapply,Bdone,Bcancel,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",E2X("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",E2X("line width"),"space=3");
   zdialog_add_widget(zd,"zspin","width","hbline","1|10|1|1","space=3");
   zdialog_add_widget(zd,"hbox","hbcircle","dialog");
   zdialog_add_widget(zd,"check","oval","hbcircle",E2X("oval"),"space=3");
   zdialog_add_widget(zd,"check","circle","hbcircle",E2X("circle"),"space=8");
   
   zdialog_restore_inputs(zd);
   zdialog_run(zd,drawoval_dialog_event,"save");
   zdialog_send_event(zd,"init");
   
   takeMouse(drawoval_mousefunc,dragcursor);
   mx1 = my1 = 0;                                                                //  no oval yet
   return;
}


//  dialog event and completion function

int drawoval_dialog_event(zdialog *zd, cchar *event)
{
   using namespace drawoval_names;

   char     color[20];
   cchar    *pp;

   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) {                                                      //  apply
         zd->zstat = 0;
         edit_apply(); 
         return 1;
      }
   
      if (zd->zstat == 2) edit_done(0);                                          //  commit edit
      else edit_cancel(0);                                                       //  discard edit
      return 1;
   }
   
   if (zstrstr("init RGB width",event))                                          //  set line color
   {
      zdialog_fetch(zd,"RGB",color,19);
      pp = strField(color,"|",1);
      if (pp) RGB[0] = atoi(pp);
      pp = strField(color,"|",2);
      if (pp) RGB[1] = atoi(pp);
      pp = strField(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,"oval",1);
      if (type == 2) zdialog_stuff(zd,"circle",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 = 1;
      else type = 2;
   }

   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 drawpoint(int px, int py, int RGB[3], int W);
   
   int      px, py, dx, dy, rx, ry, rr;
   float    a2, b2;
   float    x, y, x2, y2, cx, cy;
   
   if (Mxdrag || Mydrag)                                                         //  drag underway
   {
      PXM_free(E3pxm);                                                           //  erase prior
      E3pxm = PXM_copy(E1pxm);
      
      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
      
      if (type == 2) {                                                           //  make a circle
         if (exa > exb) exb = exa;
         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);
      }

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

      Fpaint2();
   }

   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;
   int      Eww = E3pxm->ww;
   int      Ehh = E3pxm->hh;
   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;
}


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

//  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, cchar *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      Mradius;                                                             //  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];                                                    //  Mradius <= 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 *, cchar *)                                         //  separate paint and copy
{
   using namespace paint_image_names;

   cchar    *mess1 = E2X("shift + left click: pick color from image \n"
                         "left drag: paint color on image \n"                    //  remove click actions
                         "right drag: restore original image");
   cchar    *dash = "  \xcc\xb6 ";

   F1_help_topic = "paint image";

   EFpaint.menufunc = m_paint_image;
   EFpaint.funcname = "paint_image";
   EFpaint.Farea = 2;                                                            //  select area OK
   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 [+] [-]           |
            |                                                  |
            |                                 [done] [cancel]  |
            |__________________________________________________|

   ****/

   zdialog *zd = zdialog_new(E2X("Paint on Image"),Mwin,Bdone,Bcancel,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",E2X("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","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",E2X("brush size"));
   zdialog_add_widget(zd,"label","laboc","vbbru1",Bopacitycenter);
   zdialog_add_widget(zd,"label","laboe","vbbru1",Bopacityedge);

   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","Mradius","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",E2X("paint"),"space=3");
   zdialog_add_widget(zd,"radio","erase","hbp",E2X("erase"));
   zdialog_add_widget(zd,"button","undlast","hbp",Bundolast,"space=5");
   zdialog_add_widget(zd,"button","undall","hbp",Bundoall);

   zdialog_add_widget(zd,"hbox","hbt","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","Fptran","hbt",E2X("include transparent areas"),"space=3");
   
   zdialog_add_widget(zd,"hbox","hbd","dialog");
   zdialog_add_widget(zd,"check","Fdrag","hbd",E2X("drag image"),"space=3");
   zdialog_add_widget(zd,"label","space","hbd",0,"space=10");
   zdialog_add_widget(zd,"label","labzoom","hbd",E2X("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,"Mradius",1,2,100);                                        //  stretch scales at sensitive end
   zdialog_rescale(zd,"opccent",1,2,100);
   zdialog_rescale(zd,"opcedge",0,1,100);
   
   zdialog_restore_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,"Mradius");                                             //  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, cchar *event)
{
   using namespace paint_image_names;

   cchar       *pp;
   char        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 wheel
      pp = strField(color,"|",1);
      if (pp) RGB[0] = atoi(pp);
      pp = strField(color,"|",2);
      if (pp) RGB[1] = atoi(pp);
      pp = strField(color,"|",3);
      if (pp) RGB[2] = atoi(pp);
   }
   
   if (strmatch(event,"palette")) 
      err = RGB_chooser(zd,"colorbutt",RGB);                                     //  select color from palette          19.0

   if (strmatch(event,"HSL")) {
      err = HSL_chooser(zd,"colorbutt",RGB);                                     //  select color from palette          19.0
      if (err) return 1;
      snprintf(color,20,"%d|%d|%d",RGB[0],RGB[1],RGB[2]);
      zdialog_stuff(zd,"colorbutt",color);
   }

   if (zstrstr("Mradius opccent opcedge",event))
   {
      zdialog_fetch(zd,"Mradius",Mradius);                                       //  get new brush attributes
      zdialog_fetch(zd,"opccent",opccent);
      zdialog_fetch(zd,"opcedge",opcedge);

      sprintf(text,"%3d",Mradius);                                               //  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 = Mradius;

      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;
   char        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,Mradius,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 = E3pxm->ww;
   hh = E3pxm->hh;

   paint_savepixB(px,py);                                                        //  save pixels for poss. undo

   red = RGB[0];                                                                 //  paint color
   green = RGB[1];
   blue = RGB[2];

   radius = Mradius;

   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 == 3) {                                                        //  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

      if (sa_stat == 3 && dist < sa_blendwidth)                                  //  select area edge blend,
         kern = kern * sa_blendfunc(dist);                                       //    reduce opacity

      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
      pixBmem = (pixBmem_t **) zmalloc(maxpixB * sizeof(void *));
      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 = Mradius;
   npix = 0;

   for (dy = -radius; dy <= radius; dy++)                                        //  count pixels in block
   for (dx = -radius; dx <= radius; dx++)
   {
      if (px + dx < 0 || px + dx > E3pxm->ww-1) continue;
      if (py + dy < 0 || py + dy > E3pxm->hh-1) continue;
      npix++;
   }

   cc = npix * pcc4 + pixBmem_cc;
   paintsave1 = (pixBmem_t *) zmalloc(cc);                                       //  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 > E3pxm->ww-1) continue;
      if (py + dy < 0 || py + dy > E3pxm->hh-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 > E3pxm->ww-1) continue;
         if (py + dy < 0 || py + dy > E3pxm->hh-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 > E3pxm->ww-1) continue;
         if (py + dy < 0 || py + dy > E3pxm->hh-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
{
// char        *RGB_chooser_file;                                                //  defined in fotoxx.h
   char        color[20];
   zdialog     *pzdialog;
   cchar       *pcolorbutt;
   uint8       *pRGB;
   PIXBUF      *pixbuf = 0;
   GtkWidget   *frame, *drawarea;
   STATB       statb;
}


int RGB_chooser(zdialog *pzd, cchar *pbutt, uint8 prgb[3])                       //  19.0
{
   using namespace RGB_chooser_names;

   int   RGB_chooser_dialog_event(zdialog *zd, cchar *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 || stat(RGB_chooser_file,&statb)) {                    //  default color chooser file         20.0
      RGB_chooser_file = (char *) zmalloc(200);
      snprintf(RGB_chooser_file,200,"%s/colorwheel.jpg",get_zimagedir());
   }

   /***
          ____________________________________________
         |             Color Chooser                  |
         |                                            |
         |          click on desired color            |
         |  ________________________________________  |
         | |                                        | |
         | |                                        | |
         | |                                        | |
         | |                  image                 | |
         | |                                        | |
         | |                                        | |
         | |                                        | |
         | |                                        | |
         | |________________________________________| |
         |                                            |
         | [_______________________________] [browse] |
         |                                            |
         |                                   [cancel] |
         |____________________________________________|

   ***/

   zdialog *zd = zdialog_new(E2X("Color Chooser"),Mwin,Bcancel,null);

   zdialog_add_widget(zd,"label","labclick","dialog",E2X("click on desired color"));

   zdialog_add_widget(zd,"frame","frame","dialog");
   frame = zdialog_widget(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",Bbrowse,"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, cchar *event)
{
   using namespace RGB_chooser_names;

   char     *pp;
   
   if (strmatch(event,"browse")) 
   {
      pp = navi::galleryname;                                                    //  20.0
      if (! pp) pp = topfolders[0];
      pp = gallery_select1(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) {
      printz("pixbuf error: %s \n",gerror->message);                             //  popup message >> draw event loop
      return 1;                                                                  //     GTK 3.22.11
   }

   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       19.0
   zdialog_stuff(pzdialog,pcolorbutt,color);
   
   pRGB[0] = pix1[0];                                                            //  update caller's paint color        19.0
   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;
   cchar       *pcolorbutt;
   uint8       *pRGB;
   float       H, S, L;                                                          //  chosen HSL color
   float       R, G, B;                                                          //  corresp. RGB color
}

int HSL_chooser(zdialog *pzd, cchar *pbutt, uint8 prgb[3])                       //  19.0
{
   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, cchar *event);
   
   pzdialog = pzd;                                                               //  copy args from caller
   pcolorbutt = pbutt;
   pRGB = prgb;
   
/***
       ________________________________________________
      |                                                |
      |  [#######]   [##############################]  |
      |  Color Hue   ================[]==============  |
      |  Saturation  =====================[]=========  |
      |  Lightness   ===========[]===================  |
      |                                                |
      |                                      [cancel]  |
      |________________________________________________|

***/

   zdialog *zd = zdialog_new("Adjust HSL",Mwin,Bcancel,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_widget(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_widget(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",E2X("Color Hue"));
   zdialog_add_widget(zd,"label","labsat","vb1",E2X("Saturation"));
   zdialog_add_widget(zd,"label","lablgt","vb1",E2X("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;
   char     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       19.0
   zdialog_stuff(pzdialog,pcolorbutt,color);
   
   pRGB[0] = r;                                                                  //  update caller's paint color        19.0
   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, cchar *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;
}


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

//  Copy Pixels function.
//  Copy from one image area to another with variable opacity.

namespace copypixels1
{
   int   dialog_event(zdialog* zd, cchar *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      Mradius;                                                             //  mouse radius
   int      imagex, imagey;                                                      //  source image location
   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    EFcopypix1;
}


//  menu function

void m_copypixels1(GtkWidget *, cchar *)                                         //  separate paint and copy
{
   using namespace copypixels1;

   cchar    *mess1 = E2X("shift + left click: pick position to copy \n"
                         "left click or drag: copy image to mouse \n"
                         "right click or drag: restore original image");

   F1_help_topic = "copy pixels 1";

   EFcopypix1.menufunc = m_copypixels1;
   EFcopypix1.funcname = "copy_pixels_1";
   EFcopypix1.Farea = 2;                                                         //  select area OK
   EFcopypix1.mousefunc = mousefunc;                                             //  mouse function
   if (! edit_setup(EFcopypix1)) return;                                         //  setup edit

   /***
             ____________________________________________________
            |                 Copy Pixels (1 image)              |
            |                                                    |
            |  shift + left click: pick image position to copy   |
            |  left click or drag: copy image to mouse position  |
            |  right click or drag: restore original image       |
            |                                                    |
            |  brush size      [____]     [undo last]            |
            |  opacity center  [____]     [undo all]             |
            |  opacity edge    [____]                            |
            |                                                    |
            |  [x] paint transparent areas                       |
            |                                                    |
            |                                   [done] [cancel]  |
            |____________________________________________________|

   ***/

   zdialog *zd = zdialog_new(E2X("Copy Pixels (1 image)"),Mwin,Bdone,Bcancel,null);
   EFcopypix1.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,"vbox","space","hbbri",0,"space=10");
   zdialog_add_widget(zd,"vbox","vbbr3","hbbri",0,"space=10");
   zdialog_add_widget(zd,"label","labbr","vbbr1",E2X("brush size"));
   zdialog_add_widget(zd,"label","labtc","vbbr1",Bopacitycenter);
   zdialog_add_widget(zd,"label","labte","vbbr1",Bopacityedge);
   zdialog_add_widget(zd,"zspin","Mradius","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",Bundolast);
   zdialog_add_widget(zd,"button","undall","vbbr3",Bundoall);
   zdialog_add_widget(zd,"hbox","hb4","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","Fptran","hb4",E2X("paint over transparent areas"),"space=5");

   zdialog_restore_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,"Mradius");                                             //  get kernel initialized

   totmem = 0;                                                                   //  memory used
   pixBmem = 0;                                                                  //  pixel block memory
   totpixB = 0;                                                                  //  pixel blocks
   pixBseq = 0;
   imagex = imagey = 0;                                                          //  no source pixels
   
   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 copypixels1::dialog_event(zdialog *zd, cchar *event)
{
   using namespace copypixels1;

   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("Mradius opccent opcedge",event))                                 //  get new brush attributes
   {
      zdialog_fetch(zd,"Mradius",Mradius);
      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 = Mradius;

      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 copypixels1::mousefunc()
{
   using namespace copypixels1;

   static int  pmxdown = 0, pmydown = 0;
   int         px, py;

   if (LMclick && KBshiftkey)                                                    //  shift + left mouse click
   {
      imagex = Mxclick;                                                          //  new source image location
      imagey = Myclick;
   }

   else if (LMclick || RMclick)
   {
      if (LMclick) mode = 1;                                                     //  left click, paint
      if (RMclick) mode = 2;                                                     //  right click, erase

      px = Mxdown = Mxclick;
      py = Mydown = Myclick;
 
      pixBseq++;                                                                 //  new undo seq. no.

      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
         pixBseq++;                                                              //  new undo seq. no.
         pmxdown = Mxdown;
         pmydown = Mydown;
      }

      dopixels(px,py);                                                           //  do 1 block of pixels
   }

   draw_mousecircle(Mxposn,Myposn,Mradius,0,0);                                  //  draw mouse circle

   if (mode == 1 && (Mxdown || Mydown)) {                                        //  2nd circle tracks source pixels
      px = imagex + Mxposn - Mxdown;
      py = imagey + Myposn - Mydown;
      if (px > 0 && px < E3pxm->ww-1 && py > 0 && py < E3pxm->hh-1)
         draw_mousecircle2(px,py,Mradius,0,0);
   }
   else draw_mousecircle2(0,0,0,1,0);                                            //  no 2nd circle

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


//  paint or erase 1 block of pixels within mouse radius of px, py

void copypixels1::dopixels(int px, int py)
{
   using namespace copypixels1;

   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 (! imagex && ! imagey) return;                                             //  no source area defined

   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 = E3pxm->ww;
   hh = E3pxm->hh;

   savepixB(px,py);                                                              //  save pixels for poss. undo

   radius = Mradius;

   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 == 3) {                                                        //  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

      if (sa_stat == 3 && dist < sa_blendwidth)                                  //  select area edge blend,
         kern = kern * sa_blendfunc(dist);                                       //    reduce opacity

      pix1 = PXMpix(E1pxm,qx,qy);                                                //  source image pixel
      pix3 = PXMpix(E3pxm,qx,qy);                                                //  edited image pixel

      if (mode == 1)                                                             //  paint
      {
         sx = imagex + qx - Mxdown;                                              //  image location + mouse drag shift
         sy = imagey + qy - Mydown;
         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 copypixels1::savepixB(int px, int py)
{
   using namespace copypixels1;

   int            cc, npix, radius, dx, dy;
   float          *pix3;
   pixBmem_t      *save1B;
   
   if (! pixBmem) {                                                              //  first time
      pixBmem = (pixBmem_t **) zmalloc(maxpixB * sizeof(void *));
      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 = Mradius;
   npix = 0;

   for (dy = -radius; dy <= radius; dy++)                                        //  count pixels in block
   for (dx = -radius; dx <= radius; dx++)
   {
      if (px + dx < 0 || px + dx > E3pxm->ww-1) continue;
      if (py + dy < 0 || py + dy > E3pxm->hh-1) continue;
      npix++;
   }

   cc = npix * pcc4 + pixBmem_cc;
   save1B = (pixBmem_t *) zmalloc(cc);                                           //  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 > E3pxm->ww-1) continue;
      if (py + dy < 0 || py + dy > E3pxm->hh-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 copypixels1::undolastB()
{
   using namespace copypixels1;

   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 > E3pxm->ww-1) continue;
         if (py + dy < 0 || py + dy > E3pxm->hh-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 copypixels1::freefirstB() 
{
   using namespace copypixels1;

   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 > E3pxm->ww-1) continue;
         if (py + dy < 0 || py + dy > E3pxm->hh-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 copypixels1::freeallB()
{
   using namespace copypixels1;

   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 Pixels function.
//  'Paint' a target image with pixels from a different source image.

namespace copypixels2
{
   int   dialog_event(zdialog* zd, cchar *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    EFcopypix2;

   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
      int      mpx, mpy;                                                         //  mouse drag position
      int      mrad;                                                             //  mouse radius
      int      Fvalid;                                                           //  mouse data valid flag
      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_copypixels2(GtkWidget *, cchar *)                                         //  split source/target processes
{
   using namespace copypixels2;
   
   int      err, fd;
   size_t   cc;

   fd = shm_open("/fotoxx_copy_pixels2",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 = -1;                                                             //  no pixel copy area selected
   mrad = -1;
   mmap_data->Fvalid = 0;                                                        //  no valid target mouse data yet
   mmap_data->Fscale = 1.0;                                                      //  defaule source image scale factor
   
   EFcopypix2.menufunc = m_copypixels2;                                          //  start edit function for target image
   EFcopypix2.funcname = "copy_pixels_2";
   EFcopypix2.Farea = 2;                                                         //  select area OK
   EFcopypix2.mousefunc = mousefunc;                                             //  mouse function
   if (! edit_setup(EFcopypix2)) return;     

   F1_help_topic = "copy pixels 2";

   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

   pixblockmem = (pixblockmem_t **) zmalloc(maxpixblock * sizeof(void *));       //  saved pixel blocks list
   totmem = 0;                                                                   //  memory used
   totpixblock = 0;                                                              //  pixel blocks
   pixblockseq = 0;
   
   /****
             __________________________________________________
            |              Copy Pixels (2 images)              |
            |                                                  |
            |  left click: synchronize copy position           |
            |  left click or drag: copy source image to mouse  |
            |  right click or drag: restore original image     |
            |                                                  |
            |  source image scale [____]                       |
            |                                                  |
            |  brush size      [____]     [undo last]          |
            |  opacity center  [____]     [undo all]           |
            |  opacity edge    [____]                          |
            |                                                  |
            |  [x] paint over transparent areas                |
            |                                                  |
            |                                 [done] [cancel]  |
            |__________________________________________________|

   ****/

   cchar    *mess1 = E2X("left click: synchronize copy position \n"
                         "left click or drag: copy source image to mouse \n"
                         "right click or drag: restore original image");

   zdialog *zd = zdialog_new(E2X("Copy Pixels (2 images)"),Mwin,Bdone,Bcancel,null);
   EFcopypix2.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",E2X("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",E2X("brush size"));
   zdialog_add_widget(zd,"label","labtc","vbbr1",Bopacitycenter);
   zdialog_add_widget(zd,"label","labte","vbbr1",Bopacityedge);
   zdialog_add_widget(zd,"zspin","mrad","vbbr2","1|200|1|30");
   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",Bundolast);
   zdialog_add_widget(zd,"button","undall","vbbr3",Bundoall);
   zdialog_add_widget(zd,"hbox","hb4","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","Fptran","hb4",E2X("paint over transparent areas"),"space=5");

   zdialog_restore_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

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

   save_params();                                                                //  communicate current file   bugfix  19.19
   new_session("-x 1 -m \"Copy Pixels 3\" ");                                    //  start source image fotoxx process

   return;
}


//  dialog event and completion callback function

int copypixels2::dialog_event(zdialog *zd, cchar *event)
{
   using namespace copypixels2;

   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 copypixels2::mousefunc()
{
   using namespace copypixels2;

   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)                                                                  //  left mouse click
   {
      Mxdown = Mxclick;                                                          //  click position
      Mydown = Myclick;
      mmap_data->mpxC = Mxdown;                                                  //  inform source image,
      mmap_data->mpyC = Mydown;                                                  //    new mouse drag start
      mmap_data->Fvalid = 1;
      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 copypixels2::dopixels()
{
   using namespace copypixels2;

   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 = E3pxm->ww;
   hh = E3pxm->hh;
   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 == 3) {                                                        //  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

      if (sa_stat == 3 && dist < sa_blendwidth)                                  //  select area edge blend,
         kern = kern * sa_blendfunc(dist);                                       //    reduce opacity

      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 copypixels2::save_pixblock()
{
   using namespace copypixels2;

   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 > E3pxm->ww-1) continue;
      if (mpy + dy < 0 || mpy + dy > E3pxm->hh-1) continue;
      npix++;
   }

   cc = npix * pcc + pixblockmem_cc;
   save1B = (pixblockmem_t *) zmalloc(cc);                                       //  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 > E3pxm->ww-1) continue;
      if (mpy + dy < 0 || mpy + dy > E3pxm->hh-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 copypixels2::undo_lastblock()
{
   using namespace copypixels2;

   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 > E3pxm->ww-1) continue;
         if (py + dy < 0 || py + dy > E3pxm->hh-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 copypixels2::free_firstblock() 
{
   using namespace copypixels2;

   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 > E3pxm->ww-1) continue;
         if (py + dy < 0 || py + dy > E3pxm->hh-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 copypixels2::free_allblocks()
{
   using namespace copypixels2;

   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 Pixels source image function.
//  Started by m_copypixels2() as a separate session to view the source image.

namespace copypixels3
{
   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
      int      mpx, mpy;                                                         //  mouse drag position
      int      mrad;                                                             //  mouse radius
      int      Fvalid;                                                           //  mouse data valid flag
      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_copypixels3(GtkWidget *, cchar *)                                         //  split source/target processes
{
   using namespace copypixels3;

   int      err, fd;
   int      pmpx = 0, pmpy = 0, pmrad = 0;
   size_t   cc;

   F1_help_topic = "copy pixels 2";

   fd = shm_open("/fotoxx_copy_pixels2",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;
   }
   
   char     *Pcurr_file = 0;
   int      Frefresh = 1;
   int      ww, hh;
   int      mpx2, mpy2, mrad2;
   
   mpxC = mpyC = -1;                                                             //  no pixel copy area selected
   mrad = -1;
   
   while (true)
   {
      zsleep(0.01);
      zmainloop(3);
      
      if (! curr_file) continue;
      if (mmap_data->killsource) goto done;                                      //  target image process is done

      if (! Pcurr_file || ! strmatch(curr_file,Pcurr_file)) {                    //  detect file change
         if (Pcurr_file) zfree(Pcurr_file);
         Pcurr_file = zstrdup(curr_file);
         Frefresh = 1;
      }
      
      if (CEF) {                                                                 //  source image edit, wait until done
         Frefresh = 1;
         continue;
      }
      
      if (mmap_data->Fscale != Fscale) Frefresh = 1;
      
      if (Frefresh)
      {
         printz("m_copypixels3() source image: %s \n",curr_file);                //  19.19
         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);
         mpxC = mpyC = -1;                                                       //  no copy area selected
         takeMouse(mousefunc,dragcursor);                                        //  connect mouse function
         Frefresh = 0;
      }
      
      if (mpxC < 0) continue;                                                    //  no source pixel copy area
      if (! mmap_data->Fvalid) continue;                                         //  no valid target mouse data

      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,E2X("source image failure (scale too big?)"));               //  20.0

done:
   if (CEF) edit_cancel(0);   
   printz("m_copypixels3() exit \n");
   quitxx();                                                                     //  unconditional exit                 20.05
}


//  mouse function

void copypixels3::mousefunc()
{
   using namespace copypixels3;
   
   if (LMclick) {                                                                //  left mouse click
      mpxC = Mxclick * Fscale;                                                   //  new source pixel copy area
      mpyC = Myclick * Fscale;
      LMclick = 0;
   }

   return;
}


//  copy block of pixels within mouse circle to shared memory

void copypixels3::dopixels()
{
   using namespace copypixels3;

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


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

//  Select area and edit in parallel
//  Current edit function is applied to areas painted with the mouse.
//  Mouse can be weak or strong, and edits are applied incrementally.
//  
//  method:
//  entire image is a select area with all pixel edge distance = 0 (outside area)
//  blendwidth = 10000 (infinite)
//  pixels painted with mouse have increasing edge distance to amplify edits

int   paint_edits_radius;
int   paint_edits_cpower;
int   paint_edits_epower;

void m_paint_edits(GtkWidget *, cchar *)                                         //  menu function
{
   int   paint_edits_dialog_event(zdialog *, cchar *event);                      //  dialog event function
   void  paint_edits_mousefunc();                                                //  mouse function

   cchar    *title = E2X("Paint Edits");
   cchar    *helptext = E2X("Press F1 for help");

   F1_help_topic = "paint edits";

   if (sa_stat || zd_sela) {                                                     //  warn select area will be lost
      int yn = zmessageYN(Mwin,E2X("Select area cannot be kept.\n"
                                   "Continue?"));
      if (! yn) return;
      sa_clear();                                                                //  clear area
      if (zd_sela) zdialog_free(zd_sela);
   }

   if (! CEF) {                                                                  //  edit func must be active
      zmessageACK(Mwin,E2X("Edit function must be active"));
      return;
   }
   
   if (! CEF->FusePL) {
      zmessageACK(Mwin,E2X("Cannot use Paint Edits"));
      return;
   }
   
   if (CEF->Fpreview) 
      zdialog_send_event(CEF->zd,"fullsize");                                    //  use full-size image

/***
    ______________________________________
   |         Press F1 for help            |
   |     Edit Function must be active     |
   |                                      |
   | mouse radius [____]                  |
   | power:  center [____]  edge [____]   |
   | [reset area]                         |
   |                              [done]  |
   |______________________________________|

***/

   zd_sela = zdialog_new(title,Mwin,Bdone,null);
   zdialog_add_widget(zd_sela,"label","labhelp1","dialog",helptext,"space=5");
   zdialog_add_widget(zd_sela,"label","labspace","dialog");
   zdialog_add_widget(zd_sela,"hbox","hbr","dialog",0,"space=3");
   zdialog_add_widget(zd_sela,"label","labr","hbr",Bmouseradius,"space=5");
   zdialog_add_widget(zd_sela,"zspin","radius","hbr","2|500|1|50");
   zdialog_add_widget(zd_sela,"hbox","hbt","dialog",0,"space=3");
   zdialog_add_widget(zd_sela,"label","labtc","hbt",E2X("power:  center"),"space=5");
   zdialog_add_widget(zd_sela,"zspin","center","hbt","0|100|1|50");
   zdialog_add_widget(zd_sela,"label","labte","hbt",Bedge,"space=5");
   zdialog_add_widget(zd_sela,"zspin","edge","hbt","0|100|1|0");
   zdialog_add_widget(zd_sela,"hbox","hbra","dialog",0,"space=5");
   zdialog_add_widget(zd_sela,"button","reset","hbra",E2X("reset area"),"space=5");

   paint_edits_radius = 50;
   paint_edits_cpower = 50;
   paint_edits_epower = 0;

   sa_pixmap_create();                                                           //  allocate select area pixel maps    19.0

   sa_minx = 0;                                                                  //  enclosing rectangle
   sa_maxx = Fpxb->ww;
   sa_miny = 0;
   sa_maxy = Fpxb->hh;

   sa_Npixel = Fpxb->ww * Fpxb->hh;
   sa_stat = 3;                                                                  //  area status = complete
   sa_mode = mode_image;                                                         //  area mode = whole image
   sa_calced = 1;                                                                //  edge calculation complete
   sa_blendwidth = 10000;                                                        //  "blend width"
   sa_fww = Fpxb->ww;                                                            //  valid image dimensions
   sa_fhh = Fpxb->hh;
   areanumber++;                                                                 //  next sequential number

   zdialog_run(zd_sela,paint_edits_dialog_event,"save");                         //  run dialog - parallel
   return;
}


//  Adjust whole image area to increase edit power for pixels within the mouse radius
//  sa_pixmap[*]  = 0 = never touched by mouse
//                = 1 = minimum edit power (barely painted)
//                = sa_blendwidth = maximum edit power (edit fully applied)

int paint_edits_dialog_event(zdialog *zd, cchar *event)
{
   void  paint_edits_mousefunc();                                                //  mouse function

   if (zd->zstat)                                                                //  done or cancel
   {
      freeMouse();                                                               //  disconnect mouse function
      if (CEF) zdialog_send_event(CEF->zd,"done");                               //  complete edit
      zdialog_free(zd_sela);                                                     //  kill dialog
      sa_clear();                                                                //  clear area
      return 1;
   }

   if (sa_stat != 3) return 1;                                                   //  area gone
   if (! sa_validate()) return 1;                                                //  area invalid for curr. image file

   if (strmatch(event,"focus")) {                                                //  toggle mouse capture
      if (CEF) takeMouse(paint_edits_mousefunc,0);
      else freeMouse();                                                          //  disconnect mouse
   }

   if (strmatch(event,"radius"))
      zdialog_fetch(zd,"radius",paint_edits_radius);                             //  set mouse radius

   if (strmatch(event,"center"))
      zdialog_fetch(zd,"center",paint_edits_cpower);                             //  set mouse center power

   if (strmatch(event,"edge"))
      zdialog_fetch(zd,"edge",paint_edits_epower);                               //  set mouse edge power

   if (strmatch(event,"reset")) {
      sa_clear();                                                                //  clear current area if any
      sa_pixmap_create();                                                        //  allocate select area pixel maps    19.0

      sa_minx = 0;                                                               //  enclosing rectangle
      sa_maxx = Fpxb->ww;
      sa_miny = 0;
      sa_maxy = Fpxb->hh;

      sa_Npixel = Fpxb->ww * Fpxb->hh;
      sa_stat = 3;                                                               //  area status = complete
      sa_mode = mode_image;                                                      //  area mode = whole image
      sa_calced = 1;                                                             //  edge calculation complete
      sa_blendwidth = 10000;                                                     //  "blend width"
      sa_fww = Fpxb->ww;                                                         //  valid image dimensions
      sa_fhh = Fpxb->hh;
      areanumber++;                                                              //  next sequential number
   }

   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()
{
   static int busy = 0;

   int      ii, px, py, rx, ry;
   int      radius, radius2, cpower, epower;
   float    rad, rad2, power;

   if (! CEF) return;                                                            //  no active edit
   if (sa_stat != 3) return;                                                     //  area gone?
   
   radius = paint_edits_radius;                                                  //  pixel selection radius
   radius2 = radius * radius;
   cpower = paint_edits_cpower;
   epower = paint_edits_epower;

   draw_mousecircle(Mxposn,Myposn,radius,0,0);                                   //  show mouse selection circle

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

   if (busy++) return;                                                           //  avoid re-entrance                  20.0

   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 > Fpxb->ww-1) continue;                                   //  off the image edge
      if (py < 0 || py > Fpxb->hh-1) continue;

      ii = Fpxb->ww * py + px;
      rad = sqrt(rad2);
      power = cpower + rad / radius * (epower - cpower);                         //  power at pixel radius

      if (Mbutton == 1) {                                                        //  left mouse button
         sa_pixmap[ii] += 5.0 * power;                                           //  increase edit power
         if (sa_pixmap[ii] > sa_blendwidth) sa_pixmap[ii] = sa_blendwidth;
      }

      if (Mbutton == 3) {                                                        //  right mouse button
         if (sa_pixmap[ii] <= 5.0 * power) sa_pixmap[ii] = 0;                    //  weaken edit power
         else sa_pixmap[ii] -= 5.0 * power;
      }
   }
   
   zdialog_send_event(CEF->zd,"blendwidth");                                     //  notify edit dialog
   draw_mousecircle(Mxposn,Myposn,radius,0,0);                                   //  show mouse selection circle
   busy = 0;

   return;
}


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

//  Undo effects of current edit function in areas painted with the mouse.
//  Mouse can be weak or strong, and edits are removed incrementally.
//  
//  method:
//  E3 image (edited) is incrementally replaced from E1 image (original);


namespace undo_edits_names
{
   editfunc EFundo_edits;

   PXM      *E0source, *E0edited;
   int      Eww, Ehh;
   int      radius;
   int      cpower;
   int      epower;
   
}


//  menu function

void m_undo_edits(GtkWidget *, cchar *)                                          //  menu function                      20.0
{
   using namespace undo_edits_names;

   int   undo_edits_dialog_event(zdialog *, cchar *event);                       //  dialog event function
   void  undo_edits_mousefunc();                                                 //  mouse function

   zdialog *zd;

   F1_help_topic = "undo edits";
   
   if (CEF) {                                                                    //  edit func must be active
      zmessageACK(Mwin,E2X("finish current edit first"));
      return;
   }

   if (URS_pos < 1) {
      zmessageACK(Mwin,E2X("no previous edit"));
      return;
   }
   
   if (! E0pxm) {
      zmessageACK(Mwin,E2X("no current image"));
      return;
   }

   m_undo(0,0);                                                                  //  load previous edit version
   E0source = PXM_copy(E0pxm);                                                   //    >> unedited image
  
   m_redo(0,0);                                                                  //  load current image
   E0edited = PXM_copy(E0pxm);                                                   //    >> edited image

   if (E0source->ww != E0edited->ww || E0source->hh != E0edited->hh) {
      zmessageACK(Mwin,E2X("This edit cannot be incrementally undone"));
      PXM_free(E0source);                                                        //  free memory
      PXM_free(E0edited);
      return;
   }

   Eww = E0source->ww;
   Ehh = E0source->hh;

   EFundo_edits.menufunc = m_undo_edits;                                         //  start new edit
   EFundo_edits.funcname = "undo edits";
   EFundo_edits.mousefunc = undo_edits_mousefunc;

   if (! edit_setup(EFundo_edits)) {
      PXM_free(E0source);
      PXM_free(E0edited);
      return;
   }

/***
          ______________________________________
         |           Undo Edits                 |
         |                                      |
         | mouse radius [____]                  |
         | power:  center [____]  edge [____]   |
         |                                      |
         |                      [done] [cancel] |
         |______________________________________|

***/

   zd = zdialog_new(E2X("Undo Edits"),Mwin,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hbr","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labr","hbr",Bmouseradius,"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",E2X("power:  center"),"space=5");
   zdialog_add_widget(zd,"zspin","center","hbt","0|100|1|50");
   zdialog_add_widget(zd,"label","labte","hbt",Bedge,"space=5");
   zdialog_add_widget(zd,"zspin","edge","hbt","0|100|1|0");

   EFundo_edits.zd = zd;

   radius = 200;
   cpower = 50;
   epower = 0;

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


//  dialog event and completion function

int undo_edits_dialog_event(zdialog *zd, cchar *event)
{
   using namespace undo_edits_names;

   void  undo_edits_mousefunc();                                                 //  mouse function

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

   if (zd->zstat)                                                                //  done or cancel
   {
      freeMouse();                                                               //  disconnect mouse function
      edit_done(0);                                                              //  complete edit
      PXM_free(E0source);                                                        //  free memory
      PXM_free(E0edited);
      return 1;
   }

   if (strmatch(event,"focus"))                                                  //  toggle mouse capture
      takeMouse(undo_edits_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

   return 1;
}


//  mouse function

void undo_edits_mousefunc()
{
   using namespace undo_edits_names;

   int      px, py, ww, rx, ry, radius2;
   float    rad, rad2, power, F1;
   float    R0, G0, B0, R1, G1, B1, R3, G3, B3;
   float    *pix0, *pix1, *pix3;

   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.0001 * power;                                                       //  scale 0 - 0.01
      
      pix0 = PXMpix(E3pxm,px,py);                                                //  current pixel
      pix1 = PXMpix(E0source,px,py);                                             //  original image pixel
      pix3 = PXMpix(E0edited,px,py);                                             //  edited image pixel
      
      R0 = pix0[0];                                                              //  current pixel RGB
      G0 = pix0[1];
      B0 = pix0[2];

      R1 = pix1[0];                                                              //  original pixel RGB
      G1 = pix1[1];
      B1 = pix1[2];

      R3 = pix3[0];                                                              //  edited pixel RGB
      G3 = pix3[1];
      B3 = pix3[2];
      
      if (Mbutton == 1) {                                                        //  left mouse, move to E1
         R0 += F1 * (R1 - R3);
         G0 += F1 * (G1 - G3);
         B0 += F1 * (B1 - B3);
      }

      else if (Mbutton == 3) {                                                   //  right mouse, move to E3
         R0 += F1 * (R3 - R1);
         G0 += F1 * (G3 - G1);
         B0 += F1 * (B3 - B1);
      }
      
      if (R3 > R1) {                                                             //  limit R range to R1 ... R3
         if (R0 < R1) R0 = R1;
         else if (R0 > R3) R0 = R3;
      }
      else {
         if (R0 < R3) R0 = R3;
         else if (R0 > R1) R0 = R1;
      }

      if (G3 > G1) {                                                             //  G range
         if (G0 < G1) G0 = G1;
         else if (G0 > G3) G0 = G3;
      }
      else {
         if (G0 < G3) G0 = G3;
         else if (G0 > G1) G0 = G1;
      }

      if (B3 > B1) {                                                             //  B range
         if (B0 < B1) B0 = B1;
         else if (B0 > B3) B0 = B3;
      }
      else {
         if (B0 < B3) B0 = B3;
         else if (B0 > B1) B0 = B1;
      }

      pix0[0] = R0;
      pix0[1] = G0;
      pix0[2] = B0;
   }
   
   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;
}


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

//  Plugin menu functions

namespace plugins_names
{
   #define maxplugins 100
   int         Nplugins;                                                         //  plugin menu items
   char        *plugins[100];
   GtkWidget   *popup_plugmenu = 0;
   editfunc    EFplugin;
}


//  edit plugins menu or choose and run a plugin function

void m_plugins(GtkWidget *, cchar *)
{
   using namespace plugins_names;

   void  m_edit_plugins(GtkWidget *, cchar *);
   void  m_run_plugin(GtkWidget *, cchar *);

   char        plugfile[200], buff[200], *pp;
   FILE        *fid;
   STATB       stbuff;
   int         ii, err;

   F1_help_topic = "plugins";

   if (popup_plugmenu) {
      popup_menu(Mwin,popup_plugmenu);                                           //  popup the plugins menu
      return;
   }

   snprintf(plugfile,200,"%s/plugins",get_zhomedir());                           //  plugins file

   err = stat(plugfile,&stbuff);                                                 //  exists?
   if (err)
   {
      fid = fopen(plugfile,"w");                                                 //  no, create default
      fprintf(fid,"Gimp = gimp %%s \n");
      fprintf(fid,"auto-gamma = mogrify -auto-gamma %%s \n");
      fprintf(fid,"whiteboard cleanup = mogrify "                                //  ImageMagick white board cleanup
                  "-morphology Convolve DoG:15,100,0 "
                  "-negate -normalize -blur 0x1 -channel RBG "
                  "-level 60%%,91%%,0.1 %%s \n");
      fclose(fid);
   }

   fid = fopen(plugfile,"r");                                                    //  open plugins file
   if (! fid) {
      zmessageACK(Mwin,"plugins file: %s",strerror(errno));
      return;
   }

   for (ii = 0; ii < 99; ii++)                                                   //  read list of plugins
   {
      pp = fgets_trim(buff,200,fid,1);
      if (! pp) break;
      plugins[ii] = zstrdup(buff);
   }

   fclose(fid);
   Nplugins = ii;

   popup_plugmenu = create_popmenu();                                            //  create popup menu for plugins

   add_popmenu_item(popup_plugmenu, E2X("Edit Plugins"),                         //  1st entry is Edit Plugins
                     m_edit_plugins, 0, E2X("Edit plugins menu"));

   for (ii = 0; ii < Nplugins; ii++)                                             //  add the plugin menu functions
   {
      char *pp = strstr(plugins[ii]," = ");
      if (! pp) continue;
      *pp = 0;
      add_popmenu_item(popup_plugmenu, plugins[ii], m_run_plugin, 0,
                                 E2X("Run as Fotoxx edit function"));
      *pp = ' ';
   }

   popup_menu(Mwin,popup_plugmenu);                                              //  popup the menu
   return;
}


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

//  edit plugins menu

void m_edit_plugins(GtkWidget *, cchar *)                                        //  overhauled 
{
   using namespace plugins_names;

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

   int         ii;
   char        *pp;
   zdialog     *zd;

   F1_help_topic = "plugins";

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0

/***
       ___________________________________
      |         Edit Plugins              |
      |                                   |
      |  menu name [_____________|v]      |     e.g. edit with gimp
      |  command   [___________________]  |     e.g. gimp %s
      |                                   |
      |             [Add] [Remove] [Done] |
      |___________________________________|

***/

   zd = zdialog_new(E2X("Edit Plugins"),Mwin,Badd,Bremove,Bdone,null);
   zdialog_add_widget(zd,"hbox","hbm","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labm","hbm",E2X("menu name"),"space=5");
   zdialog_add_widget(zd,"comboE","menuname","hbm",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbc","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labc","hbc",E2X("command"),"space=5");
   zdialog_add_widget(zd,"zentry","command","hbc",0,"space=5|expand");

   for (ii = 0; ii < Nplugins; ii++)                                             //  stuff combo box with available menus
   {
      pp = strstr(plugins[ii]," = ");                                            //  menu name = command line
      if (! pp) continue;
      *pp = 0;
      zdialog_cb_app(zd,"menuname",plugins[ii]);
      *pp = ' ';
   }

   zdialog_set_modal(zd);
   zdialog_run(zd,edit_plugins_event,"mouse");
   return;
}


//  dialog event function

int edit_plugins_event(zdialog *zd, cchar *event)
{
   using namespace plugins_names;

   int      ii, jj, cc, zstat;
   char     *pp, menuname[100], command[200];
   char     buff[200];
   FILE     *fid;

   if (strmatch(event,"menuname"))
   {
      zdialog_fetch(zd,"menuname",menuname,100);

      for (ii = 0; ii < Nplugins; ii++)                                          //  find selected menu name
      {
         pp = strstr(plugins[ii]," = ");
         if (! pp) continue;
         *pp = 0;
         jj = strmatch(menuname,plugins[ii]);
         *pp = ' ';
         if (jj) {
            zdialog_stuff(zd,"command",pp+3);                                    //  stuff corresp. command in dialog
            break;
         }
      }

      return 1;
   }

   zstat = zd->zstat;                                                            //  wait for dialog completion
   if (! zstat) return 1;

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

   if (zstat == 1)                                                               //  add plugin or replace same menu name
   {
      zd->zstat = 0;                                                             //  keep dialog active

      if (Nplugins == maxplugins) {
         zmessageACK(Mwin,"too many plugins");
         return 1;
      }

      zdialog_fetch(zd,"menuname",menuname,100);
      zdialog_fetch(zd,"command",command,200);

      pp = strstr(command," %s");
      if (! pp) zmessageACK(Mwin,"Warning: command without \"%%s\" ");

      for (ii = 0; ii < Nplugins; ii++)                                          //  find existing plugin record
      {
         pp = strstr(plugins[ii]," = ");
         if (! pp) continue;
         *pp = 0;
         jj = strmatch(menuname,plugins[ii]);
         *pp = ' ';
         if (jj) break;
      }

      if (ii == Nplugins) {                                                      //  new plugin record
         plugins[ii] = 0;
         Nplugins++;
         zdialog_cb_app(zd,"menuname",menuname);
         pp = zdialog_cb_get(zd,"menuname",ii);
      }

      if (plugins[ii]) zfree(plugins[ii]);
      cc = strlen(menuname) + strlen(command) + 4;
      plugins[ii] = (char *) zmalloc(cc);
      *plugins[ii] = 0;
      strncatv(plugins[ii],cc,menuname," = ",command,0);
      return 1;                                                                  //  19.0
   }

   if (zstat == 2)                                                               //  remove current plugin
   {
      zd->zstat = 0;                                                             //  keep dialog active

      zdialog_fetch(zd,"menuname",menuname,100);

      for (ii = 0; ii < Nplugins; ii++)                                          //  find existing plugin record
      {
         pp = strstr(plugins[ii]," = ");
         if (! pp) continue;
         *pp = 0;
         jj = strmatch(menuname,plugins[ii]);
         *pp = ' ';
         if (jj) break;
      }

      if (ii == Nplugins) return 1;                                              //  not found

      zfree(plugins[ii]);                                                        //  remove plugin record
      Nplugins--;
      for (jj = ii; jj < Nplugins; jj++)
         plugins[jj] = plugins[jj+1];
      zdialog_cb_delete(zd,"menuname",menuname);                                 //  delete entry from combo box
      zdialog_stuff(zd,"menuname","");
      zdialog_stuff(zd,"command","");
      return 1;                                                                  //  19.0
   }

   if (zstat == 3)                                                               //  done
   {
      snprintf(buff,199,"%s/plugins",get_zhomedir());                            //  open file for plugins
      fid = fopen(buff,"w");
      if (! fid) {
         zmessageACK(Mwin,strerror(errno));
         return 1;
      }

      for (int ii = 0; ii < Nplugins; ii++)                                      //  save plugins
         if (plugins[ii])
            fprintf(fid,"%s \n",plugins[ii]);

      fclose(fid);

      zdialog_free(zd);
      popup_plugmenu = 0;                                                        //  rebuild popup menu
      return 1;                                                                  //  19.0
   }

   return 1;
}


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

//  process plugin menu selection
//  execute corresponding command using current image file

void m_run_plugin(GtkWidget *, cchar *menu)
{
   using namespace plugins_names;

   int         ii, jj, err;
   char        *pp = 0, plugincommand[200], pluginfile[100];
   PXM         *pxmtemp;
   zdialog     *zd = 0;

   F1_help_topic = "plugins";

   for (ii = 0; ii < Nplugins; ii++)                                             //  search plugins for menu name
   {
      pp = strchr(plugins[ii],'=');                                              //  match menu name to plugin command
      if (! pp) continue;                                                        //  menu name = ...
      *pp = 0;
      jj = strmatch(plugins[ii],menu);
      *pp = '=';
      if (jj) break;
   }

   if (ii == Nplugins) {
      zmessageACK(Mwin,"plugin menu not found %s",menu);
      return;
   }
   
   strncpy0(plugincommand,pp+1,200);                                             //  corresp. command
   strTrim2(plugincommand);

   pp = strstr(plugincommand,"%s");                                              //  no file placeholder in command
   if (! pp) {
      err = shell_ack(plugincommand);                                            //  execute non-edit plugin command
      goto RETURN;
   }
   
   EFplugin.menufunc = m_run_plugin;
   EFplugin.funcname = menu;
   if (! edit_setup(EFplugin)) return;                                           //  start edit function

   snprintf(pluginfile,100,"%s/plugfile.tif",temp_folder);                       //  .../tempfiles-nnnnnn/plugfile.tif

   err = PXM_save(E1pxm,pluginfile,8,100,1);                                     //  E1 >> plugin_file
   if (err) goto FAIL;

   zd = zmessage_post(Mwin,"20/20",0,E2X("Plugin working ..."));

   repl_1str(plugincommand,command,"%s",pluginfile);                             //  command filename

   err = shell_ack(command);                                                     //  execute plugin command
   if (err) goto FAIL;
   
   pxmtemp = TIFF_PXM_load(pluginfile);                                          //  read command output file
   if (! pxmtemp) {
      zmessageACK(Mwin,E2X("plugin failed"));
      goto FAIL;
   }

   PXM_free(E3pxm);                                                              //  plugin_file >> E3
   E3pxm = pxmtemp;

   CEF->Fmods++;                                                                 //  assume image was modified
   CEF->Fsaved = 0;
   edit_done(0);
   goto RETURN;

FAIL:
   edit_cancel(0);

RETURN:
   if (zd) zdialog_free(zd);
   return;
}


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

//  menu function: open a camera RAW file and edit with the Raw Therapee GUI
//  opens 'rawfile' if not null, else 'clicked_file' if not null

void m_rawtherapee(GtkWidget *, cchar *)
{
   char * rawtherapee_findtiff(cchar *pattern);

   char     *pp;
   char     *rawfile, *tiffile = 0;
   int      err;
   STATB    statb;

   F1_help_topic = "raw therapee";

   if (! Frawtherapee) {
      zmessageACK(Mwin,E2X("Raw Therapee not installed"));
      return;
   }

   if (checkpend("busy block mods")) return;

   rawfile = 0;

   if (clicked_file) {
      rawfile = clicked_file;
      clicked_file = 0;
   }
   
   else if (curr_file) 
      rawfile = zstrdup(curr_file);

   if (! rawfile) return;
   
   if (image_file_type(rawfile) != RAW) {
      zmessageACK(Mwin,E2X("RAW type not registered in User Preferences"));
      zfree(rawfile);
      return;
   }
   
   pp = zescape_quotes(rawfile);
   shell_ack("rawtherapee \"%s\"",pp);
   zfree(pp);

   tiffile = raw_to_tiff(rawfile);                                               //  tif file with base name = RAW
   zfree(rawfile);

   err = stat(tiffile,&statb);                                                   //  look first in RAW file folder
   if (! err) goto found;

   pp = strrchr(tiffile,'/');                                                    //  not found, search top folders      20.06
   pp = rawtherapee_findtiff(pp+1);
   if (! pp) goto retx;                                                          //  not found

   zfree(tiffile);                                                               //  found
   tiffile = zstrdup(pp);

found:
   err = f_open(tiffile,0,0,1);                                                  //  open tiff file
   if (err) goto retx;

   update_image_index(tiffile);                                                  //  update index rec.

retx:
   if (tiffile) zfree(tiffile);
   else zmessage_post(Mwin,"20/20",3,E2X("Raw Therapee produced no tif file"));
   m_viewmode(0,"F");
   return;
}


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

//  Find first image file matching a base name pattern.
//  All top image folders and subfolders are searched.
//  The base file name pattern may have * and ? wildcards.

char * rawtherapee_findtiff(cchar *pattern)                                      //  20.06
{
   int      ii;
   char     command[300], buff[500];
   FILE     *fid;
   char     *pp;
   
   for (ii = 0; ii < Ntopfolders; ii++) {
      snprintf(command,300,"find \"%s\" -name \"%s\" -print -quit", topfolders[ii], pattern);
      fid = popen(command,"r");
      if (! fid) continue;
      pp = fgets_trim(buff,500,fid);
      pclose(fid);
      if (pp) return pp;
   }
   
   return 0;
}


