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

   Fotoxx      edit photos and manage collections

   Copyright 2007-2016 Michael Cornelison
   Source URL: http://kornelix.net
   Contact: kornelix@posteo.de

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

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

   You should have received a copy of the GNU General Public License
   along with this program. If not, see http://www.gnu.org/licenses/.

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

   Fotoxx image edit - Edit menu functions

   m_trimrotate            trim/crop and rotate combination
   m_upright               upright a rotated image
   m_voodoo1               automatic image enhancement with limited smarts
   m_voodoo2               automatic image enhancement with limited smarts
   m_combo                 adjust brightness, contrast, color, white balance
   m_britedist             flatten and/or expand brightness distribution
   m_zonal_flatten         flatten zonal brightness distribution
   m_tonemap               apply adjustable tone mapping to an image
   m_resize                resize (rescale) image
   m_flip                  horizontal or vertical mirror image
   m_write_text            write text on an image
   gentext                 create text graphic with attributes and transparency
   m_write_line            write lines or arrows on an image
   genline                 create line/arrow graphic with attributes and transparency
   m_paint_edits           paint edit function gradually with the mouse
   m_lever_edits           apply edit function leveraged by RGB level or contrast
   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

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

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

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

//  trim (crop) and/or rotate an image                                           //  combine trim and rotate
//  preview mode and related code removed                                        //  16.04

//    fotoxx.h:
//    int      trimrect[4];
//    int      trimsize[2];

namespace trimrotate
{
   int      ptrimrect[4];                                                        //  prior trim rectangle (this image)
   int      ptrimsize[2];                                                        //  prior trim size (previous image)
   float    trimR;                                                               //  trim ratio, width/height
   float    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, KBcorner, Frotate;
   int      Fmax = 0;

   editfunc EFtrimrotate;

   void   dialog();
   int    dialog_event(zdialog *zd, cchar *event);
   void   trim_customize();
   int    trim_customize_dialog_event(zdialog *zd, cchar *event);
   void   mousefunc();
   void   KBfunc(int key);
   void   trim_limits();
   void   trim_darkmargins();
   void   trim_final();
   void   rotate_func();
   void   drawlines();
   void   autotrim();
}


//  menu function

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

   int         ii, xmargin, ymargin;
   cchar       *trim_message = ZTX("Trim: drag middle to move, drag corners to resize");
   cchar       *rotate_mess = ZTX("Minor rotate: drag right edge with mouse");
   char        text[20];
   zdialog     *zd;
   
   F1_help_topic = "trim_rotate";

   EFtrimrotate.menufunc = m_trimrotate;                                         //  menu function
   EFtrimrotate.funcname = "trim_rotate";
   EFtrimrotate.Frestart = 1;                                                    //  allow restart
   EFtrimrotate.mousefunc = mousefunc;
   if (! edit_setup(EFtrimrotate)) return;                                       //  setup edit
   
   PXM_addalpha(E0pxm);                                                          //  15.09
   PXM_addalpha(E1pxm);
   PXM_addalpha(E3pxm);

   E0ww = E0pxm->ww;                                                             //  full-size image dimensions
   E0hh = E0pxm->hh;
   E3ww = E3pxm->ww;                                                             //  preview image (possibly smaller)
   E3hh = E3pxm->hh;

   if (menu && strmatch(menu,"keep")) {                                          //  keep preset trim rectangle
      trimsize[0] = trimrect[2] - trimrect[0];                                   //  (full image scale)
      trimsize[1] = trimrect[3] - trimrect[1];
      trimR = 1.0 * trimsize[0] / trimsize[1];                                   //  trim ratio = width/height
   }

   else
   {
      trimsize[0] = 0.8 * E0ww;                                                  //  initial trim rectangle, 80% image size
      trimsize[1] = 0.8 * E0hh;
      xmargin = 0.5 * (E0ww - trimsize[0]);                                      //  set balanced margins
      ymargin = 0.5 * (E0hh - trimsize[1]);
      trimrect[0] = xmargin;
      trimrect[2] = E0ww - xmargin;
      trimrect[1] = ymargin;
      trimrect[3] = E0hh - ymargin;
      trimR = 1.0 * trimsize[0] / trimsize[1];                                   //  trim ratio = width/height
   }

   ptrimrect[0] = 0;                                                             //  set prior trim rectangle
   ptrimrect[1] = 0;                                                             //    = 100% of image
   ptrimrect[2] = E3ww;
   ptrimrect[3] = E3hh;

   rotate_goal = rotate_angle = 0;                                               //  initially no rotation

/***
       ___________________________________________________________
      |                                                           |
      |     Trim: drag middle to move, drag corners to resize     |
      |                                                           |
      |  width [____|-|+]  height [____|-|+]   ratio 1.5          |
      |  trim size: [Max] [Prev] [Invert] [Auto]  [x] Lock Ratio  |
      |  [1:1] [2:1] [3:2] [4:3] [16:9] [gold] [Customize]        |
      |                                                           |
      |  Minor rotate: drag right edge with mouse   [level]       |
      |  Rotate: degrees [____|-+]                                |              //  auto-trim removed                  16.06
      |                                                           |
      |  Rotate 90° [image] [image]   180° [image]                |
      |                                                           |
      |                            [Grid] [Apply] [Done] [Cancel] |
      |___________________________________________________________|

***/

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

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

   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"label","labW","hb1",Bwidth,"space=5");
   zdialog_add_widget(zd,"spin","width","hb1","20|20000|1|1000");
   zdialog_add_widget(zd,"label","space","hb1",0,"space=5");
   zdialog_add_widget(zd,"label","labH","hb1",Bheight,"space=5");
   zdialog_add_widget(zd,"spin","height","hb1","20|20000|1|600");
   zdialog_add_widget(zd,"label","space","hb1",0,"space=5");
   zdialog_add_widget(zd,"label","labR","hb1",ZTX("ratio"),"space=5");
   zdialog_add_widget(zd,"label","ratio","hb1","1.67   ");

   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"label","lab2","hb2",ZTX("trim size:"),"space=5");
   zdialog_add_widget(zd,"button","max","hb2",Bmax,"space=5");
   zdialog_add_widget(zd,"button","prev","hb2",Bprev,"space=5");
   zdialog_add_widget(zd,"button","invert","hb2",Binvert,"space=5");
   zdialog_add_widget(zd,"button","auto","hb2",Bauto,"space=5");
   zdialog_add_widget(zd,"check","lock","hb2",ZTX("Lock Ratio"),"space=15");
   
   zdialog_add_ttip(zd,"max",ZTX("maximize trim box"));
   zdialog_add_ttip(zd,"prev",ZTX("use previous size"));
   zdialog_add_ttip(zd,"invert",ZTX("invert width/height"));
   zdialog_add_ttip(zd,"auto",ZTX("trim transparent edges"));
   zdialog_add_ttip(zd,"lock",ZTX("lock width/height ratio"));

   zdialog_add_widget(zd,"hbox","hb3","dialog");                                       //  ratio buttons inserted here
   for (ii = 0; ii < 6; ii++)
      zdialog_add_widget(zd,"button",trimbuttons[ii],"hb3",trimbuttons[ii],"space=5");
   zdialog_add_widget(zd,"button","custom","hb3",ZTX("Customize"),"space=5");          //  and [custom] button

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

   zdialog_add_widget(zd,"hbox","hb4","dialog");                                       //  15.12
   zdialog_add_widget(zd,"label","labrotmess","hb4",rotate_mess,"space=5");
   zdialog_add_widget(zd,"button","level","hb4",ZTX("Level"),"space=10");
   zdialog_add_ttip(zd,"level",ZTX("use EXIF data if available"));

   zdialog_add_widget(zd,"hbox","hb5","dialog");
   zdialog_add_widget(zd,"label","labrotate","hb5",ZTX("Rotate: degrees"),"space=5");
   zdialog_add_widget(zd,"spin","degrees","hb5","-360|360|0.1|0");

   zdialog_add_widget(zd,"hbox","hb90","dialog");
   zdialog_add_widget(zd,"label","lab90","hb90","Rotate 90°","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,"label","lab180","hb90","180°","space=8");
   zdialog_add_widget(zd,"imagebutt","180","hb90","rotate-180.png","size=32");

   zd = EFtrimrotate.zd;

   zdialog_restore_inputs(zd);                                                   //  restore all prior inputs

   zdialog_fetch(zd,"width",ptrimsize[0]);                                       //  preserve prior trim size
   zdialog_fetch(zd,"height",ptrimsize[1]);                                      //    for use by [prev] button
   zdialog_stuff(zd,"width",trimsize[0]);                                        //  stuff width, height, ratio as
   zdialog_stuff(zd,"height",trimsize[1]);                                       //    pre-calculated for this image
   snprintf(text,20,"%.2f  ",trimR);
   zdialog_stuff(zd,"ratio",text);
   zdialog_stuff(zd,"degrees",0);

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

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

   if (Fmax) {
      Fmax = 0;
      zdialog_send_event(zd,"max");
   }
   
   return;
}


//  dialog event and completion callback function

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

   static int  flip = 0;
   int         width, height, delta;
   int         ii, rlock;
   float       r1, r2, ratio = 0;
   char        text[20];
   cchar       *pp;
   cchar       *exifkey[1] = { exif_rollangle_key };
   char        *ppv[1];
   
   if (strmatch(event,"done")) zd->zstat = 3;                                    //  apply and quit
   if (strmatch(event,"enter")) zd->zstat = 3;
   if (strmatch(event,"cancel")) zd->zstat = 4;                                  //  cancel
   if (strmatch(event,"escape")) zd->zstat = 4;                                  //  escape = cancel                    15.07

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

      if (zd->zstat == 2) {                                                      //  [apply]
         trim_final();
         CEF->Fmods++;
         edit_done(0);
         m_trimrotate(0,0);                                                      //  restart edit
         return 1;
      }

      if (zd->zstat == 3) {                                                      //  [done]
         trim_final();
         CEF->Fmods++;
         currgrid = 0;                                                           //  restore normal grid settings       15.09
         edit_done(0);
         return 1;
      }

      draw_toplines(2);                                                          //  cancel - erase trim rectangle
      erase_topcircles();                                                        //    and target circle
      currgrid = 0;                                                              //  restore normal grid settings
      edit_cancel(0);
      return 1;
   }

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

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

   if (strmatch(event,"prev"))                                                   //  use width/height from prior
   {                                                                             //    image trim
      width = ptrimsize[0];
      height = ptrimsize[1];
      if (width < 20 || height < 20) return 1;                                   //  no value established, ignore
      if (width > E0ww) width = E0ww;
      if (height > E0hh) height = E0hh;
      zdialog_stuff(zd,"width",width);
      zdialog_stuff(zd,"height",height);
      event = "width";                                                           //  process same as manual inputs
   }

   if (strstr("width height",event))                                             //  get direct width/height inputs
   {
      zdialog_fetch(zd,"width",width);                                           //  full image scale
      zdialog_fetch(zd,"height",height);

      zdialog_fetch(zd,"lock",rlock);                                            //  lock ratio on/off

      if (strmatch(event,"width")) {
         if (width > E3ww) width = E3ww;
         if (rlock) {                                                            //  ratio locked
            height = width / trimR + 0.5;                                        //  try to keep ratio
            if (height > E3hh) height = E3hh;
         }
      }

      if (strmatch(event,"height")) {
         if (height > E3hh) height = E3hh;
         if (rlock) {
            width = height * trimR + 0.5;
            if (width > E3ww) width = E3ww;
         }
      }

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

      delta = width - trimsize[0];

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

      if (delta < 0) {                                                           //  decreased width
         trimrect[0] = trimrect[0] - delta / 2;
         trimrect[2] = trimrect[2] + delta / 2;
         if (delta % 2) {
            trimrect[0] = trimrect[0] + flip;
            trimrect[2] = trimrect[2] - 1 + flip;
         }
      }

      delta = height - trimsize[1];

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

      if (delta < 0) {                                                           //  decreased height
         trimrect[1] = trimrect[1] - delta / 2;
         trimrect[3] = trimrect[3] + delta / 2;
         if (delta % 2) {
            trimrect[1] = trimrect[1] + flip;
            trimrect[3] = trimrect[3] - 1 + flip;
         }
      }

      if (trimrect[0] < 0) trimrect[0] = 0;                                      //  keep within limits
      if (trimrect[2] > E3ww) trimrect[2] = E3ww;                                //  use ww not ww-1
      if (trimrect[1] < 0) trimrect[1] = 0;
      if (trimrect[3] > E3hh) trimrect[3] = E3hh;

      width = trimrect[2] - trimrect[0];                                         //  new width and height
      height = trimrect[3] - trimrect[1];

      if (width > E3ww) width = E3ww;                                            //  limit to actual size
      if (height > E3hh) height = E3hh;

      trimsize[0] = width;                                                       //  new trim size
      trimsize[1] = height;

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

      if (! rlock)                                                               //  set new ratio if not locked
         trimR = 1.0 * trimsize[0] / trimsize[1];

      snprintf(text,20,"%.2f  ",trimR);                                          //  stuff new ratio
      zdialog_stuff(zd,"ratio",text);

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

   if (strmatch(event,"max"))                                                    //  maximize trim rectangle
   {
      trimrect[0] = 0;
      trimrect[1] = 0;
      trimrect[2] = E3ww;
      trimrect[3] = E3hh;

      width = E3ww;                                                              //  full scale trim size
      height = E3hh;

      trimsize[0] = width;                                                       //  new trim size
      trimsize[1] = height;

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

      trimR = 1.0 * trimsize[0] / trimsize[1];
      snprintf(text,20,"%.2f  ",trimR);                                          //  stuff new ratio
      zdialog_stuff(zd,"ratio",text);
      zdialog_stuff(zd,"lock",0);                                                //  set no ratio lock

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

   if (strmatch(event,"invert"))                                                 //  invert width/height dimensions
      ratio = 1.0 / trimR;                                                       //  mark ratio changed

   for (ii = 0; ii < 6; ii++)                                                    //  trim ratio buttons
      if (strmatch(event,trimbuttons[ii])) break;
   if (ii < 6) {
      r1 = r2 = ratio = 0;
      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) ratio = r1/r2;
      if (ratio < 0.1 || ratio > 10) ratio = 1.0;
      if (! ratio) return 1;
      zdialog_stuff(zd,"lock",1);                                                //  assume lock is wanted
      trimR = ratio;
   }

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

      if (trimrect[2] - trimrect[0] > trimrect[3] - trimrect[1])
         trimrect[3] = trimrect[1] + (trimrect[2] - trimrect[0]) / trimR;        //  adjust smaller dimension
      else
         trimrect[2] = trimrect[0] + (trimrect[3] - trimrect[1]) * trimR;

      if (trimrect[2] > E3ww) {                                                  //  if off the right edge,
         trimrect[2] = E3ww;                                                     //  adjust height
         trimrect[3] = trimrect[1] + (trimrect[2] - trimrect[0]) / trimR;
      }

      if (trimrect[3] > E3hh) {                                                  //  if off the bottom edge,
         trimrect[3] = E3hh;                                                     //  adjust width
         trimrect[2] = trimrect[0] + (trimrect[3] - trimrect[1]) * trimR;
      }

      trimsize[0] = trimrect[2] - trimrect[0];                                   //  new rectangle dimensions
      trimsize[1] = trimrect[3] - trimrect[1];

      zdialog_stuff(zd,"width",trimsize[0]);                                     //  stuff width, height, ratio
      zdialog_stuff(zd,"height",trimsize[1]);
      snprintf(text,20,"%.2f  ",trimR);
      zdialog_stuff(zd,"ratio",text);

      trim_darkmargins();                                                        //  update trim area in image
      return 1;
   }
   
   if (strmatch(event,"level"))                                                  //  auto level using EXIF RollAngle    15.12
   {
      exif_get(curr_file,exifkey,ppv,1);  
      if (ppv[0]) {
         rotate_goal = atof(ppv[0]);
         zfree(ppv[0]);
      }
   }

   if (strstr("+90 -90 180 degrees",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 (strmatch(event,"degrees"))                                             //  degrees adjustment
         zdialog_fetch(zd,"degrees",rotate_goal);
   }
   
   if (rotate_goal)
   {
      if (rotate_goal > 180) rotate_goal -= 360;                                 //  keep within -180 to +180 deg.
      if (rotate_goal < -180) rotate_goal += 360;
      if (fabsf(rotate_goal) < 0.01) rotate_goal = 0;                            //  = 0 within my precision

      zdialog_stuff(zd,"degrees",rotate_goal);
      dialog_event(zd,"max");                                                    //  max. trim rectangle
      rotate_func();                                                             //  E3 is rotated E1

      if (strstr("+90 -90 180",event))                                           //  assume no trim wanted
         dialog_event(zd,"max");
   }

   if (strmatch(event,"auto"))                                                   //  auto trim margins                  15.09
   {
      if (CEF->Fmods) {                                                          //  save pending edit
         dialog_event(zd,"max");
         trim_final();
         edit_done(0);
      }
      else edit_cancel(0);
      autotrim();                                                                //  do auto-trim
      m_trimrotate(0,"keep");                                                    //  restart edit
      return 1;
   }

   return 1;
}


//  dialog to get 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;

   zdialog *zd = zdialog_new(ZTX("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",ZTX("label"),"space=3");
   zdialog_add_widget(zd,"label","ratio","hbr",ZTX("ratio"),"space=3");

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

   for (ii = 0; ii < 6; ii++)                                                    //  add current trimbuttons to dialog
   {
      blab[5] = '0' + ii;
      rlab[6] = '0' + ii;
      zdialog_add_widget(zd,"entry",blab,"hbb",trimbuttons[ii],"size=6");
      zdialog_add_widget(zd,"entry",rlab,"hbr",trimratios[ii],"size=6");
   }

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

   if (zstat == 1)                                                               //  done
   {
      for (ii = 0; ii < 6; 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 < 6; 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;
}


//  dialog event and completion function

int trimrotate::trim_customize_dialog_event(zdialog *zd, cchar *event)
{
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07
   return 1;
}


//  trim/rotate mouse function

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

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

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

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

   if (RMclick)                                                                  //  remove the lines
   {
      RMclick = 0;
      Fcorner = 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_rotate2;                                              //  continue rotate in progress

   else if (Fcorner)
   {
      if (Fcorner == 1) { trimrect[0] = mpx; trimrect[1] = mpy; }                //  continue dragging corner with mouse
      if (Fcorner == 2) { trimrect[2] = mpx; trimrect[1] = mpy; }
      if (Fcorner == 3) { trimrect[2] = mpx; trimrect[3] = mpy; }
      if (Fcorner == 4) { trimrect[0] = mpx; trimrect[3] = mpy; }
   }

   else
   {
      moveall = 1;
      dd = 0.2 * (trimrect[2] - trimrect[0]);                                    //  test if mouse is in the broad
      if (mpx < trimrect[0] + dd) moveall = 0;                                   //    middle of the rectangle
      if (mpx > trimrect[2] - dd) moveall = 0;
      dd = 0.2 * (trimrect[3] - trimrect[1]);
      if (mpy < trimrect[1] + dd) moveall = 0;
      if (mpy > trimrect[3] - dd) moveall = 0;
   }

   if (moveall) {                                                                //  yes, move the whole rectangle
      trimrect[0] += xdrag;
      trimrect[2] += xdrag;
      trimrect[1] += ydrag;
      trimrect[3] += ydrag;
   }

   else if (! Fcorner)                                                           //  no, find closest corner to mouse
   {
      dx = mpx - trimrect[0];
      dy = mpy - trimrect[1];
      d1 = sqrt(dx*dx + dy*dy);                                                  //  distance from NW corner

      dx = mpx - trimrect[2];
      dy = mpy - trimrect[1];
      d2 = sqrt(dx*dx + dy*dy);                                                  //  NE

      dx = mpx - trimrect[2];
      dy = mpy - trimrect[3];
      d3 = sqrt(dx*dx + dy*dy);                                                  //  SE

      dx = mpx - trimrect[0];
      dy = mpy - trimrect[3];
      d4 = sqrt(dx*dx + dy*dy);                                                  //  SW

      Fcorner = 1;                                                               //  NW
      dd = d1;
      if (d2 < dd) { Fcorner = 2; dd = d2; }                                     //  NE
      if (d3 < dd) { Fcorner = 3; dd = d3; }                                     //  SE
      if (d4 < dd) { Fcorner = 4; dd = d4; }                                     //  SW

      if (E3ww - mpx < dd && mpx > 0.9 * E3ww)                                   //  mouse closer to right edge than corner
         if (mpy > 0.3 * E3hh && mpy < 0.7 * E3hh)                               //  if far from corners assume rotate
            goto mouse_rotate1;

      if (Fcorner == 1) { trimrect[0] = mpx; trimrect[1] = mpy; }                //  move this corner to mouse
      if (Fcorner == 2) { trimrect[2] = mpx; trimrect[1] = mpy; }
      if (Fcorner == 3) { trimrect[2] = mpx; trimrect[3] = mpy; }
      if (Fcorner == 4) { trimrect[0] = mpx; trimrect[3] = mpy; }
   }

   KBcorner = Fcorner;                                                           //  remember last corner moved

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

   return;

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

   mouse_rotate1:
   
   zdialog_send_event(zd,"max");                                                 //  v.15.11

   mouse_rotate2:

   Frotate = 1;
   Fcorner = 0;

   rotate_goal += 30.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 (fabsf(rotate_goal) < 0.01) rotate_goal = 0;                               //  = 0 within my precision

   zdialog_stuff(zd,"degrees",rotate_goal);

   rotate_func();                                                                //  E3 is rotated E1
   return;
}


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

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

   int      xstep, ystep;

   if (! Fcorner) Fcorner = KBcorner;

   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 (Fcorner == 1) {                                                           //  NW
      trimrect[0] += xstep;
      trimrect[1] += ystep;
   }

   if (Fcorner == 2) {                                                           //  NE
      trimrect[2] += xstep;
      trimrect[1] += ystep;
   }

   if (Fcorner == 3) {                                                           //  SE
      trimrect[2] += xstep;
      trimrect[3] += ystep;
   }

   if (Fcorner == 4) {                                                           //  SW
      trimrect[0] += xstep;
      trimrect[3] += ystep;
   }

   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;
   float       drr;
   char        text[20];
   zdialog     *zd = EFtrimrotate.zd;

   if (trimrect[0] > trimrect[2]-10) trimrect[0] = trimrect[2]-10;               //  sanity limits
   if (trimrect[1] > trimrect[3]-10) trimrect[1] = trimrect[3]-10;

   zdialog_fetch(zd,"lock",rlock);                                               //  w/h ratio locked
   if (rlock && Fcorner) {
      if (Fcorner < 3)
         trimrect[1] = trimrect[3] - 1.0 * (trimrect[2] - trimrect[0]) / trimR;  //  reversed                           16.01
      else
         trimrect[3] = trimrect[1] + 1.0 * (trimrect[2] - trimrect[0]) / trimR;
   }

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

   if (trimrect[2] > E3ww) {
      trimrect[2] = E3ww;
      chop = 2;
   }

   if (trimrect[1] < 0) {
      trimrect[1] = 0;
      chop = 3;
   }

   if (trimrect[3] > E3hh) {
      trimrect[3] = E3hh;
      chop = 4;
   }

   if (rlock && chop) {                                                          //  keep ratio if off edge
      if (chop < 3)
         trimrect[3] = trimrect[1] + 1.0 * (trimrect[2] - trimrect[0]) / trimR;
      else
         trimrect[2] = trimrect[0] + 1.0 * (trimrect[3] - trimrect[1]) * trimR;
   }

   if (trimrect[0] > trimrect[2]-10) trimrect[0] = trimrect[2]-10;               //  sanity limits
   if (trimrect[1] > trimrect[3]-10) trimrect[1] = trimrect[3]-10;

   if (trimrect[0] < 0) trimrect[0] = 0;                                         //  keep within visible area
   if (trimrect[2] > E3ww) trimrect[2] = E3ww;
   if (trimrect[1] < 0) trimrect[1] = 0;
   if (trimrect[3] > E3hh) trimrect[3] = E3hh;

   trimsize[0] = trimrect[2] - trimrect[0];                                      //  new rectangle dimensions
   trimsize[1] = trimrect[3] - trimrect[1];

   drr = 1.0 * trimsize[0] / trimsize[1];                                        //  new w/h ratio
   if (! rlock) trimR = drr;

   zdialog_stuff(zd,"width",trimsize[0]);                                        //  stuff width, height, ratio
   zdialog_stuff(zd,"height",trimsize[1]);
   snprintf(text,20,"%.2f  ",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);

   if (trimrect[0] < 0) trimrect[0] = 0;                                         //  keep within visible area
   if (trimrect[2] > E3ww) trimrect[2] = E3ww;
   if (trimrect[1] < 0) trimrect[1] = 0;
   if (trimrect[3] > E3hh) trimrect[3] = E3hh;

   if (ptrimrect[0] < trimrect[0]) ox1 = ptrimrect[0];                           //  outer rectangle
   else ox1 = trimrect[0];
   if (ptrimrect[2] > trimrect[2]) ox2 = ptrimrect[2];
   else ox2 = trimrect[2];
   if (ptrimrect[1] < trimrect[1]) oy1 = ptrimrect[1];
   else oy1 = trimrect[1];
   if (ptrimrect[3] > trimrect[3]) oy2 = ptrimrect[3];
   else oy2 = trimrect[3];

   if (ptrimrect[0] > trimrect[0]) nx1 = ptrimrect[0];                           //  inner rectangle
   else nx1 = trimrect[0];
   if (ptrimrect[2] < trimrect[2]) nx2 = ptrimrect[2];
   else nx2 = trimrect[2];
   if (ptrimrect[1] > trimrect[1]) ny1 = ptrimrect[1];
   else ny1 = trimrect[1];
   if (ptrimrect[3] < trimrect[3]) ny2 = ptrimrect[3];
   else ny2 = trimrect[3];

   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;

   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 < trimrect[0] || px >= trimrect[2] ||                               //  px >=                              15.10
          py < trimrect[1] || py >= trimrect[3]) {
         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);
   }

   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 < trimrect[0] || px >= trimrect[2] || 
          py < trimrect[1] || py >= trimrect[3]) {
         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 < trimrect[0] || px >= trimrect[2] || 
          py < trimrect[1] || py >= trimrect[3]) {
         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 < trimrect[0] || px >= trimrect[2] || 
          py < trimrect[1] || py >= trimrect[3]) {
         pix3[0] = pix1[0] / 2;
         pix3[1] = pix1[1] / 2;
         pix3[2] = pix1[2] / 2;
      }
      else memcpy(pix3,pix1,pcc);
   }
   
   Fpaint3(ox1,oy1,ox2-ox1,ny1-oy1);                                             //  4 updated rectangles
   Fpaint3(nx2,oy1,ox2-nx2,oy2-oy1);
   Fpaint3(ox1,ny2,ox2-ox1,oy2-ny2);
   Fpaint3(ox1,oy1,nx1-ox1,oy2-oy1);

   drawlines();                                                                  //  draw trim rectangle etc.

   ptrimrect[0] = trimrect[0];                                                   //  set prior trim rectangle
   ptrimrect[2] = trimrect[2];                                                   //    from current trim rectangle
   ptrimrect[1] = trimrect[1];
   ptrimrect[3] = trimrect[3];

   Fpaint2();
   return;
}


//  final trim - cut margins off
//  E3 is the input image, rotated and with black edges cropped if chosen
//  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);
   
   draw_toplines(2);                                                             //  erase trim rectangle
   erase_topcircles();                                                           //    and target circle

   if (trimrect[2] > E3ww) trimrect[2] = E3ww;                                   //  final check
   if (trimrect[3] > E3hh) trimrect[3] = E3hh;

   trimsize[0] = trimrect[2] - trimrect[0];                                      //  new rectangle dimensions
   trimsize[1] = trimrect[3] - trimrect[1];

   PXM_free(E9pxm);
   E9pxm = PXM_make(trimsize[0],trimsize[1],nc);                                 //  new pixmap with requested size

   for (py1 = trimrect[1]; py1 < trimrect[3]; py1++)                             //  copy E3 (rotated) to new size
   for (px1 = trimrect[0]; px1 < trimrect[2]; px1++)
   {
      px2 = px1 - trimrect[0];
      py2 = py1 - trimrect[1];
      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 and edges cropped if chosen.
//  E9 = E3 copy: used as source for darkened pixels in E3.

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

   zdialog     *zd = EFtrimrotate.zd;

   paintlock(1);                                                                 //  block window updates               16.02

   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(E9pxm);                                                           //  E1 >> E9
      E9pxm = PXM_copy(E1pxm);
   }

   if (rotate_goal != rotate_angle)
   {
      rotate_angle = rotate_goal;

      PXM_free(E9pxm);
      E9pxm = PXM_rotate(E1pxm,rotate_angle);                                    //  E9 is rotated E1
      PXM_free(E3pxm);                                                           //  E3 = E9
      E3pxm = PXM_copy(E9pxm);
   }

   paintlock(0);                                                                 //  unblock window updates             16.02

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

   if (trimrect[2] > E3ww) trimrect[2] = E3ww;                                   //  contract trim rectangle if needed
   if (trimrect[3] > E3hh) trimrect[3] = E3hh;

   trimsize[0] = trimrect[2] - trimrect[0];                                      //  new rectangle dimensions
   trimsize[1] = trimrect[3] - trimrect[1];

   zdialog_stuff(zd,"width",trimsize[0]);                                        //  stuff width, height
   zdialog_stuff(zd,"height",trimsize[1]);

   Fpaintnow();
   drawlines();                                                                  //  draw trim rectangle etc.

   ptrimrect[0] = 0;                                                             //  set prior trim rectangle
   ptrimrect[1] = 0;
   ptrimrect[2] = E3ww;                                                          //    = 100% of image
   ptrimrect[3] = E3hh;
   
   CEF->Fmods++;
   return;
}


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

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

   int      px, py, radius;

   Ntoplines = 4;                                                                //  outline trim rectangle
   toplines[0].x1 = trimrect[0];
   toplines[0].y1 = trimrect[1];
   toplines[0].x2 = trimrect[2];
   toplines[0].y2 = trimrect[1];
   toplines[0].type = 1;
   toplines[1].x1 = trimrect[2];
   toplines[1].y1 = trimrect[1];
   toplines[1].x2 = trimrect[2];
   toplines[1].y2 = trimrect[3];
   toplines[1].type = 1;
   toplines[2].x1 = trimrect[2];
   toplines[2].y1 = trimrect[3];
   toplines[2].x2 = trimrect[0];
   toplines[2].y2 = trimrect[3];
   toplines[2].type = 1;
   toplines[3].x1 = trimrect[0];
   toplines[3].y1 = trimrect[3];
   toplines[3].x2 = trimrect[0];
   toplines[3].y2 = trimrect[1];
   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);                                                             //  draw trim rectangle and guidelines

   erase_topcircles();
   px = 0.5 * (ptrimrect[0] + ptrimrect[2]);
   py = 0.5 * (ptrimrect[1] + ptrimrect[3]);
   radius = 0.02 * Mscale * (E3ww + E3hh);
   Fpaint3(px-radius,py-radius,2*radius,2*radius);

   px = 0.5 * (trimrect[0] + trimrect[2]);
   py = 0.5 * (trimrect[1] + trimrect[3]);
   add_topcircle(px,py,radius/2);
   draw_topcircles();

   if (gridsettings[currgrid][GON]) Fpaint2();                                   //  refresh gridlines, delayed 

   return;
}


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

void trimrotate::autotrim()                                                      //  15.06
{
   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 (! E0pxm) return;                                                          //  no edit image
   pxm = E0pxm;
   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] < 1) continue;                                           //    in transparent zones             15.09
            ppix = PXMpix(pxm,qx2,qy1);
            if (ppix[3] < 1) continue;
            ppix = PXMpix(pxm,qx2,qy2);
            if (ppix[3] < 1) continue;
            ppix = PXMpix(pxm,qx1,qy2);
            if (ppix[3] < 1) 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                15.09
               if (ppix[3] < 1) break;
               ppix = PXMpix(pxm,qx,qy2);
               if (ppix[3] < 1) break;
            }
            if (qx < qx2) continue;

            for (qy = qy1; qy < qy2; qy++) {                                     //  also left/right sides
               ppix = PXMpix(pxm,qx1,qy);
               if (ppix[3] < 1) break;
               ppix = PXMpix(pxm,qx2,qy);
               if (ppix[3] < 1) 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;
   }

   trimrect[0] = px1;                                                            //  set parameters for trim function
   trimrect[2] = px2;
   trimrect[1] = py1;
   trimrect[3] = py2;

   return;
}


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

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

void m_upright(GtkWidget *, cchar *menu)                                         //  15.03.1
{
   int upright_dialog_event(zdialog *zd, cchar *event);
   
   zdialog  *zd;

   F1_help_topic = "upright";

   if (checkpend("all")) return;

   if (clicked_file) {                                                           //  use clicked file if present        16.06
      f_open(clicked_file,0,0,1,0);
      clicked_file = 0;
   }
   
   if (! curr_file) {
      if (zdupright) zdialog_free(zdupright);
      zdupright = 0;
      return;
   }

   if (! zdupright) {
      zd = zdialog_new(ZTX("Upright Image"),Mwin,null);
      zdupright = zd;
      zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=5");                  //  16.06
      zdialog_add_widget(zd,"label","labf","hbf",ZTX("Image File:"),"space=3");
      zdialog_add_widget(zd,"label","file","hbf","filename.jpg","space=5");
      zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
      zdialog_add_widget(zd,"button","upright","hb1",ZTX("Upright"),"space=5");
      zdialog_add_widget(zd,"imagebutt","-90","hb1","rotate-left.png","size=32|space=5");
      zdialog_add_widget(zd,"imagebutt","+90","hb1","rotate-right.png","size=32|space=5");
      zdialog_add_widget(zd,"imagebutt","180","hb1","rotate-180.png","size=32|space=5");
      zdialog_add_widget(zd,"imagebutt","cancel","hb1","cancel.png","size=32|space=5");
      zdialog_run(zd,upright_dialog_event,"save");
   }
   
   zd = zdupright;

   char *pp = strrchr(curr_file,'/');                                            //  stuff file name in dialog          16.06
   if (pp) zdialog_stuff(zd,"file",pp+1);

   return;
}


//  dialog event and completion function

int upright_dialog_event(zdialog *zd, cchar *event)                              //  15.03.1
{
   int      angle = 0;
   char     orientation = 0, *ppv[1];
   cchar    *exifkey[1] = { exif_orientation_key };
   cchar    *exifdata[1];
   
   if (! curr_file) return 1;

   if (strmatch(event,"escape")) event = "cancel";                               //  escape = cancel                    15.07

   if (zd->zstat) {                                                              //  [x] button
      zdialog_free(zd);
      zdupright = 0;
      return 1;
   }

   if (strmatch(event,"cancel")) {                                               //  cancel button
      zdialog_free(zd);
      zdupright = 0;
      return 1;
   }

   if (! strstr("upright -90 +90 180",event)) return 1;                          //  ignore other events
   
   if (strmatch(event,"upright"))                                                //  auto upright button
   {
      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,ZTX("rotation unknown"));
         return 1;
      }
   }

   if (strmatch(event,"+90")) angle += 90;                                       //  other buttons                      16.03
   if (strmatch(event,"-90")) angle -= 90;
   if (strmatch(event,"180")) angle += 180;

   if (! angle) return 1;                                                        //  ignore other events
   
   if (checkpend("all")) return 1;                                               //  15.12

   paintlock(1);                                                                 //  block window updates               16.02
   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;
   }
   paintlock(0);                                                                 //  unblock window updates             16.02

   f_save(curr_file,curr_file_type,curr_file_bpc,1);

   PXM_free(E0pxm);

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

   if (FGWM == 'F') gtk_window_present(MWIN);                                    //  keep focus on main window          16.06
   if (FGWM == 'G') gallery(curr_file,"paint");                                  //  if gallery view, repaint           16.06

   return 1;
}


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

//  automatic image tuneup without user guidance

float       voodoo1_brdist[256];                                                 //  pixel count per brightness level
int         voodoo_01, voodoo_99;                                                //  1% and 99% brightness levels (0 - 255)
void        voodoo1_distribution();                                              //  compute brightness distribution
void *      voodoo1_thread(void *);
editfunc    EFvoodoo1;


void m_voodoo1(GtkWidget *, cchar *menu)
{
   F1_help_topic = "voodoo";

   EFvoodoo1.menuname = menu;
   EFvoodoo1.menufunc = m_voodoo1;
   EFvoodoo1.funcname = "voodoo1";
   EFvoodoo1.Farea = 1;                                                          //  select area ignored
   EFvoodoo1.Fscript = 1;                                                        //  scripting supported                15.10
   EFvoodoo1.threadfunc = voodoo1_thread;
   if (! edit_setup(EFvoodoo1)) return;                                          //  setup edit

   voodoo1_distribution();                                                       //  compute brightness distribution
   signal_thread();                                                              //  start update thread
   edit_done(0);                                                                 //  edit done
   return;
}


//  compute brightness distribution for image

void voodoo1_distribution()
{
   int         px, py, ii;
   int         ww = E1pxm->ww, hh = E1pxm->hh;
   float       bright1;
   float       *pix1;

   for (ii = 0; ii < 256; ii++)                                                  //  clear brightness distribution data
      voodoo1_brdist[ii] = 0;

   for (py = 0; py < hh; py++)                                                   //  compute brightness distribution
   for (px = 0; px < ww; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);
      bright1 = pixbright(pix1);
      voodoo1_brdist[int(bright1)]++;
   }

   for (ii = 1; ii < 256; ii++)                                                  //  cumulative brightness distribution
      voodoo1_brdist[ii] += voodoo1_brdist[ii-1];                                //   0 ... (ww * hh)

   voodoo_01 = 0;
   voodoo_99 = 255;

   for (ii = 0; ii < 256; ii++)                                                  //  compute index values (0 - 255)
   {                                                                             //    for darkest 1% and brightest 1%
      if (voodoo1_brdist[ii] < 0.01 * ww * hh) voodoo_01 = ii;
      if (voodoo1_brdist[ii] < 0.99 * ww * hh) voodoo_99 = ii;
   }

   for (ii = 0; ii < 256; ii++)
      voodoo1_brdist[ii] = voodoo1_brdist[ii]                                    //  multiplier per brightness level
                    / (ww * hh) * 256.0 / (ii + 1);                              //  ( = 1 for a flat distribution)
   return;
}


//  thread function - use multiple working threads

void * voodoo1_thread(void *)
{
   void  * voodoo1_wthread(void *arg);

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

      for (int ii = 0; ii < NWT; ii++)                                           //  start worker threads
         start_wthread(voodoo1_wthread,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

      CEF->Fmods++;
      CEF->Fsaved = 0;
      Fpaint2();                                                                 //  update window
   }

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

void * voodoo1_wthread(void *arg)                                                //  worker thread function
{
   int         index = *((int *) (arg));
   int         px, py;
   float       *pix1, *pix3;
   float       bright1, bright2, bright3, cmax;
   float       red1, green1, blue1, red3, green3, blue3;
   float       flat1 = 0.3;                                                      //  strength of distribution flatten
   float       flat2 = 1.0 - flat1;
   float       sat1 = 0.3, sat2;                                                 //  strength of saturation increase
   float       f1, f2;
   float       expand = 256.0 / (voodoo_99 - voodoo_01 + 1);                     //  brightness range expander

   for (py = index; py < E1pxm->hh; py += NWT)                                   //  voodoo brightness distribution
   for (px = 0; px < E1pxm->ww; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      red1 = pix1[0];
      green1 = pix1[1];
      blue1 = pix1[2];

      bright2 = 0.25 * red1 + 0.65 * green1 + 0.10 * blue1;                      //  input brightness, 0 - 256
      bright2 = (bright2 - voodoo_01) * expand;                                  //  expand to clip low / high 1%
      if (bright2 < 0) bright2 = 0;
      if (bright2 > 255) bright2 = 255;

      bright1 = voodoo1_brdist[int(bright2)];                                    //  factor for flat output brightness
      bright3 = flat1 * bright1 + flat2;                                         //  attenuate

      f1 = (256.0 - bright2) / 256.0;                                            //  bright2 = 0 - 256  >>  f1 = 1 - 0
      f2 = 1.0 - f1;                                                             //  prevent banding in bright areas
      bright3 = f1 * bright3 + f2;                                               //  tends to 1.0 for brighter pixels

      red3 = red1 * bright3;                                                     //  blend new and old brightness
      green3 = green1 * bright3;
      blue3 = blue1 * bright3;

      bright3 = 0.333 * (red3 + green3 + blue3);                                 //  mean color brightness
      sat2 = sat1 * (256.0 - bright3) / 256.0;                                   //  bright3 = 0 - 256  >>  sat2 = sat1 - 0
      red3 = red3 + sat2 * (red3 - bright3);                                     //  amplified color, max for dark pixels
      green3 = green3 + sat2 * (green3 - bright3);
      blue3 = blue3 + sat2 * (blue3 - bright3);

      if (red3 < 0) red3 = 0;                                                    //  stop possible underflow
      if (green3 < 0) green3 = 0;
      if (blue3 < 0) blue3 = 0;

      cmax = red3;                                                               //  stop overflow, keep color balance
      if (green3 > cmax) cmax = green3;
      if (blue3 > cmax) cmax = blue3;
      if (cmax > 255.9) {
         cmax = 255.9 / cmax;
         red3 = red3 * cmax;
         green3 = green3 * cmax;
         blue3 = blue3 * cmax;
      }

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

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


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

//  flatten brightness distribution based on the distribution of nearby zones

namespace voodoo2_names
{
   float    flatten = 30;                          //  flatten value, 30%
   float    deband = 70;                           //  deband value, 70%
   int      Iww, Ihh;                              //  image dimensions
   int      NZ;                                    //  no. of image zones
   int      Zsize, Zrows, Zcols;                   //  zone parameters
   float    *Br;                                   //  Br[py][px]  pixel brightness
   int      *Zxlo, *Zylo, *Zxhi, *Zyhi;            //  Zxlo[ii] etc.  zone ii pixel range
   int      *Zcen;                                 //  Zcen[ii][2]  zone ii center (py,px)
   uint8    *Zn;                                   //  Zn[py][px][9]  9 nearest zones for pixel: 0-200 (255 = none)
   uint8    *Zw;                                   //  Zw[py][px][9]  zone weights for pixel: 0-100 = 1.0
   float    *Zff;                                  //  Zff[ii][1000]  zone ii flatten factors

   editfunc EFvoodoo2;                             //  edit function
   void * wthread(void *);                         //  working thread
}


void m_voodoo2(GtkWidget *, cchar *menu)                                         //  15.04
{
   using namespace voodoo2_names;

   int      gNZ, pNZ;
   int      px, py, cx, cy;
   int      rx, ii, jj, kk;
   int      zww, zhh, row, col;
   float    *pix1, bright;
   float    weight[9], sumweight;
   float    D, Dthresh;
   
   F1_help_topic = "voodoo";

   EFvoodoo2.menuname = menu;
   EFvoodoo2.menufunc = m_voodoo2;
   EFvoodoo2.funcname = "voodoo2";
   EFvoodoo2.Farea = 1;                                                          //  select area ignored
   EFvoodoo2.Fscript = 1;                                                        //  scripting supported                15.10

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

   Iww = E1pxm->ww;                                                              //  image dimensions
   Ihh = E1pxm->hh;
   
   if (Iww * Ihh > 227 * MEGA) {                                                 //  16.03
      zmessageACK(Mwin,"image too big");
      edit_cancel(0);
      return;
   }

   Ffuncbusy = 1;

   NZ = 40;                                                                      //  zone count
   gNZ = pNZ = NZ;                                                               //  zone count goal

   while (true)
   {
      Zsize = sqrtf(Iww * Ihh / gNZ);                                            //  approx. zone size
      Zrows = Ihh / Zsize + 0.5;                                                 //  get appropriate rows and cols
      Zcols = Iww / Zsize + 0.5;
      NZ = Zrows * Zcols;                                                        //  NZ matching rows x cols
      if (NZ >= pNZ) break;
      gNZ++;
   }

   Br = (float *) zmalloc(Iww * Ihh * sizeof(float));                            //  allocate memory
   Zn = (uint8 *) zmalloc(Iww * Ihh * 9 * sizeof(uint8));                        //  cannot exceed 2048 M
   Zw = (uint8 *) zmalloc(Iww * Ihh * 9 * sizeof(uint8));
   Zxlo = (int *) zmalloc(NZ * sizeof(int));
   Zylo = (int *) zmalloc(NZ * sizeof(int));
   Zxhi = (int *) zmalloc(NZ * sizeof(int));
   Zyhi = (int *) zmalloc(NZ * sizeof(int));
   Zcen = (int *) zmalloc(NZ * 2 * sizeof(int));
   Zff = (float *) zmalloc(NZ * 1000 * sizeof(float));

   for (py = 0; py < Ihh; py++)                                                  //  get initial pixel brightness levels
   for (px = 0; px < Iww; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);
      bright = pixbright(pix1);
      ii = py * Iww + px;
      Br[ii] = bright;
      zmainloop(1000);                                                           //  keep GTK alive
   }

   zww = Iww / Zcols;                                                            //  actual zone size
   zhh = Ihh / Zrows;

   for (row = 0; row < Zrows; row++)
   for (col = 0; col < Zcols; col++)                                             //  set low/high bounds per zone
   {
      ii = row * Zcols + col;
      Zxlo[ii] = col * zww;
      Zylo[ii] = row * zhh;
      Zxhi[ii] = Zxlo[ii] + zww;
      Zyhi[ii] = Zylo[ii] + zhh;
      Zcen[2*ii] = Zylo[ii] + zhh/2;
      Zcen[2*ii+1] = Zxlo[ii] + zww/2;
   }

   for (ii = 0; ii < NZ; ii++)                                                   //  compute brightness distributiion
   {                                                                             //    for each zone
      for (jj = 0; jj < 1000; jj++)
         Zff[1000*ii+jj] = 0;

      for (py = Zylo[ii]; py < Zyhi[ii]; py++)                                   //  brightness distribution
      for (px = Zxlo[ii]; px < Zxhi[ii]; px++)
      {
         pix1 = PXMpix(E1pxm,px,py);
         bright = 1000.0 / 256.0 * pixbright(pix1);
         Zff[1000*ii+int(bright)]++;
      }

      for (jj = 1; jj < 1000; jj++)                                              //  cumulative brightness distribution
         Zff[1000*ii+jj] += Zff[1000*ii+jj-1];

      for (jj = 0; jj < 1000; jj++)                                              //  multiplier per brightness level
         Zff[1000*ii+jj] = Zff[1000*ii+jj] / (zww*zhh) * 1000 / (jj+1);          //    to make distribution flat

      zmainloop(1000);                                                           //  keep GTK alive
   }

   for (py = 0; py < Ihh; py++)                                                  //  set 9 nearest zones per pixel
   for (px = 0; px < Iww; px++)
   {
      rx = (py * Iww + px) * 9;                                                  //  index for 9 nearest zones to px/py

      row = py / zhh;                                                            //  zone containing pixel
      col = px / zww;

      ii = 0;
      for (jj = row-1; jj <= row+1; jj++)                                        //  loop 3x3 surrounding zones
      for (kk = col-1; kk <= col+1; kk++)
      {
         if (jj >= 0 && jj < Zrows && kk >= 0 && kk < Zcols)                     //  zone is not off the edge
            Zn[rx+ii] = jj * Zcols + kk;
         else Zn[rx+ii] = 255;                                                   //  edge row/col: missing neighbor
         ii++;
      }
   }

   if (zww < zhh)                                                                //  pixel to zone distance threshold
      Dthresh = 1.5 * zww;                                                       //  area influence = 0 beyond this distance
   else Dthresh = 1.5 * zhh;

   for (py = 0; py < Ihh; py++)                                                  //  set zone weights per pixel
   for (px = 0; px < Iww; px++)
   {
      rx = (py * Iww + px) * 9;                                                  //  index for 9 nearest zones to px/py

      for (ii = 0; ii < 9; ii++)                                                 //  distance to each zone center
      {
         jj = Zn[rx+ii];
         if (jj == 255) {
            weight[ii] = 0;                                                      //  missing zone weight = 0
            continue;
         }
         cy = Zcen[2*jj];
         cx = Zcen[2*jj+1];
         D = sqrtf((py-cy)*(py-cy) + (px-cx)*(px-cx));                           //  distance from pixel to zone center
         D = D / Dthresh;
         if (D > 1) D = 1;                                                       //  zone influence reaches zero at Dthresh
         weight[ii] = 1.0 - D;
      }

      sumweight = 0;
      for (ii = 0; ii < 9; ii++)                                                 //  zone weights based on distance
         sumweight += weight[ii];

      for (ii = 0; ii < 9; ii++)                                                 //  normalize weights, sum = 1.0
         Zw[rx+ii] = 100 * weight[ii] / sumweight;                               //  0-100 = 1.0

      zmainloop(1000);                                                           //  keep GTK alive
   }

   for (int ii = 0; ii < NWT; ii++)                                              //  start worker thread per processor core
      start_wthread(wthread,&Nval[ii]);
   wait_wthreads();                                                              //  wait for completion

   zfree(Br);                                                                    //  free memory
   zfree(Zn);
   zfree(Zw);
   zfree(Zxlo);
   zfree(Zylo);
   zfree(Zxhi);
   zfree(Zyhi);
   zfree(Zcen);
   zfree(Zff);

   Ffuncbusy = 0;
   CEF->Fmods++;
   CEF->Fsaved = 0;
   edit_done(0);                                                                 //  edit done
   return;
}


//  worker thread for each CPU processor core

void * voodoo2_names::wthread(void *arg)
{
   using namespace voodoo2_names;

   int         index = *((int *) (arg));
   int         px, py, rx, ii, jj;
   float       *pix1, *pix3;
   float       fold, fnew, cmax;
   float       red1, green1, blue1, red3, green3, blue3;
   float       FF, weight, bright, debandx;

   for (py = index; py < Ihh; py += NWT)                                         //  loop all image pixels
   for (px = 0; px < Iww; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel

      red1 = pix1[0];
      green1 = pix1[1];
      blue1 = pix1[2];

      bright = 0.25 * red1 + 0.65 * green1 + 0.10 * blue1;                       //  input pixel brightness 0-255.9
      bright *= 1000.0 / 256.0;                                                  //  0-999

      rx = (py * Iww + px) * 9;                                                  //  index for 9 nearest zones to px/py

      FF = 0;
      for (ii = 0; ii < 9; ii++) {                                               //  loop 9 nearest zones
         weight = Zw[rx+ii];
         if (weight > 0) {                                                       //  0-100 = 1.0
            jj = Zn[rx+ii];
            FF += 0.01 * weight * Zff[1000*jj+int(bright)];                      //  sum weight * flatten factor
         }
      }

      red3 = FF * red1;                                                          //  fully flattened brightness
      green3 = FF * green1;
      blue3 = FF * blue1;

      debandx = (1000.0 - bright) / 1000.0;                                      //  bright = 0 to 1000  >>  debandx = 1 to 0
      debandx += (1.0 - debandx) * (1.0 - 0.01 * deband);                        //  debandx = 1 to 1  >>  1 to 0

      fnew = 0.01 * flatten;                                                     //  how much to flatten, 0 to 1
      fnew = debandx * fnew;                                                     //  attenuate flatten of brighter pixels
      fold = 1.0 - fnew;                                                         //  how much to retain, 1 to 0

      red3 = fnew * red3 + fold * red1;                                          //  blend new and old brightness
      green3 = fnew * green3 + fold * green1;
      blue3 = fnew * blue3 + fold * blue1;

      cmax = red3;                                                               //  stop overflow, keep color balance
      if (green3 > cmax) cmax = green3;
      if (blue3 > cmax) cmax = blue3;
      if (cmax > 255.9) {
         cmax = 255.9 / cmax;
         red3 = red3 * cmax;
         green3 = green3 * cmax;
         blue3 = blue3 * cmax;
      }

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

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


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

//  Retouch Combo
//  Adjust all aspects of brightness, contrast, color.
//  Brightness curves are used, overall and per RGB color.
//  Color saturation and white balance adjustments are also available.

namespace combo_names
{
   editfunc    EFcombo;
   int         combo_spc;                                                        //  current spline curve
   float       brightness;                                                       //  brightness input, -1 ... +1
   float       contrast;                                                         //  contrast input, -1 ... +1
   float       wbalR, wbalG, wbalB;                                              //  white balance, white basis standard
   int         wbtemp;                                                           //  illumination temp. input, 1K ... 10K
   float       tempR, tempG, tempB;                                              //  RGB of illumination temp.
   float       combR, combG, combB;                                              //  combined RGB factors
   float       satlevel;                                                         //  saturation input, -1 ... +1 = saturated
   float       areaemph;                                                         //  emphasis input, -1 ... +1 = bright areas
   float       emphcurve[256];                                                   //  emphasis per brightness level, 0 ... 1
   int         Fapply = 0;                                                       //  flag, apply dialog controls to image
   int         Fdist = 0;                                                        //  flag, show brightness distribution
   float       amplify = 1.0;                                                    //  curve amplifier, 0 ... 2
   GtkWidget   *drawwin_dist, *drawwin_scale;                                    //  brightness distribution graph widgets
   int         RGBW[4] = { 0, 0, 0, 0 };                                         //     "  colors: red/green/blue/white (all)
}

void  blackbodyRGB(int K, float &R, float &G, float &B);


//  menu function

void m_combo(GtkWidget *, cchar *menu)
{
   using namespace combo_names;

   int    combo_dialog_event(zdialog* zd, cchar *event);
   void   combo_curvedit(int spc);
   void * combo_thread(void *);

   F1_help_topic = "retouch_combo";

   EFcombo.menuname = menu;
   EFcombo.menufunc = m_combo;
   EFcombo.funcname = "retouch_combo";
   EFcombo.FprevReq = 1;                                                         //  use preview
   EFcombo.Farea = 2;                                                            //  select area usable
   EFcombo.Frestart = 1;                                                         //  restart allowed
   EFcombo.FusePL = 1;                                                           //  use with paint/lever edits OK
   EFcombo.Fscript = 1;                                                          //  scripting supported                15.10
   EFcombo.threadfunc = combo_thread;
   if (! edit_setup(EFcombo)) return;                                            //  setup edit

/***
       _____________________________________________________
      |                    Retouch Combo                    |
      |  _________________________________________________  |
      | |                                                 | |                    //  5 curves are maintained:
      | |                                                 | |                    //  curve 0: current display curve
      | |                                                 | |                    //        1: curve for all colors
      | |         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
      |                                                     |
      |  Amplifier  ================[]=============  Max.   |                    //  curve amplifier
      |  Brightness ================[]=============  High   |                    //  brightness
      |   Contrast  ================[]=============  High   |                    //  contrast
      |   Low Color ====================[]=========  High   |                    //  color saturation
      |    Warmer   ====================[]========= Cooler  |                    //  color temperature
      |  Dark Areas ==========[]=================== Bright  |                    //  color emphasis
      |                                                     |
      |  [x] Brightness Distribution                        |
      |  [x] Click for white balance or black level         |                    //  click gray/white spot for white balance
      |                                                     |
      |  Settings File [Load] [Save]                        |
      |                                                     |
      |                      [Reset] [Prev] [Done] [Cancel] |
      |_____________________________________________________|

***/

   zdialog *zd = zdialog_new(ZTX("Retouch Combo"),Mwin,Breset,Bprev,Bdone,Bcancel,null);
   EFcombo.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");
   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","hbcolor","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","space","hbcolor",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbcolor1","hbcolor",0,"homog");
   zdialog_add_widget(zd,"label","space","hbcolor",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbcolor2","hbcolor",0,"homog|expand");
   zdialog_add_widget(zd,"label","space","hbcolor",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbcolor3","hbcolor",0,"homog");
   zdialog_add_widget(zd,"label","space","hbcolor",0,"space=5");

   zdialog_add_widget(zd,"label","labamp","vbcolor1",ZTX("Amplifier"));
   zdialog_add_widget(zd,"label","labrite","vbcolor1",ZTX("Brightness"));
   zdialog_add_widget(zd,"label","labcont","vbcolor1",ZTX("Contrast"));
   zdialog_add_widget(zd,"label","labsat1","vbcolor1",ZTX("Low Color"));
   zdialog_add_widget(zd,"label","labtemp1","vbcolor1",ZTX("Warmer"));
   zdialog_add_widget(zd,"label","labarea1","vbcolor1",ZTX("Dark Areas"));

   zdialog_add_widget(zd,"hscale","amplify","vbcolor2","0.0|2.0|0.01|1.0");
   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","satlevel","vbcolor2","-1.0|1.0|0.01|0");
   zdialog_add_widget(zd,"hscale","wbtemp","vbcolor2","2000|8000|1|5000");
   zdialog_add_widget(zd,"hscale","areaemph","vbcolor2","0|1.0|0.01|0.5");

   zdialog_add_widget(zd,"label","labrite2","vbcolor3",ZTX("Max."));
   zdialog_add_widget(zd,"label","labrite2","vbcolor3",ZTX("High"));
   zdialog_add_widget(zd,"label","labcont2","vbcolor3",ZTX("High"));
   zdialog_add_widget(zd,"label","labsat2","vbcolor3",ZTX("High"));
   zdialog_add_widget(zd,"label","labtemp2","vbcolor3",ZTX("Cooler"));
   zdialog_add_widget(zd,"label","labarea2","vbcolor3",ZTX("Bright"));

   zdialog_add_widget(zd,"hbox","hbdist","dialog");
   zdialog_add_widget(zd,"check","dist","hbdist",ZTX("Brightness Distribution"),"space=3");

   zdialog_add_widget(zd,"hbox","hbclick","dialog");
   zdialog_add_widget(zd,"check","click","hbclick",ZTX("Click for white balance or black level"),"space=3");
   zdialog_add_ttip(zd,"click",ZTX("check the box, then click on white or dark \n"
                                   "area to calibrate image white or black color"));

   zdialog_add_widget(zd,"hbox","hbset","dialog");
   zdialog_add_widget(zd,"label","labset","hbset",ZTX("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,ZTX("recall previous settings used"));

   zdialog_rescale(zd,"brightness",-1,0,1);                                      //  15.01
   zdialog_rescale(zd,"contrast",-1,0,1);
   zdialog_rescale(zd,"satlevel",-1,0,1);
   zdialog_rescale(zd,"wbtemp",2000,5000,8000);

   GtkWidget *frameH = zdialog_widget(zd,"frameH");                              //  setup edit curves
   spldat *sd = splcurve_init(frameH,combo_curvedit);
   EFcombo.curves = sd;

   sd->Nscale = 1;                                                               //  diagonal fixed line, neutral curve
   sd->xscale[0][0] = 0.00;
   sd->yscale[0][0] = 0.00;
   sd->xscale[1][0] = 1.00;
   sd->yscale[1][0] = 1.00;

   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;                                                          //  15.10
      sd->apx[ii][0] = sd->apy[ii][0] = 0.01;
      sd->apx[ii][1] = sd->apy[ii][1] = 0.50;                                    //  curve 0 = overall brightness
      sd->apx[ii][2] = sd->apy[ii][2] = 0.99;                                    //  curve 1/2/3 = R/G/B adjustment
      splcurve_generate(sd,ii);
   }

   sd->Nspc = 4;                                                                 //  4 curves
   sd->fact[0] = 1;                                                              //  curve 0 active                     15.10
   combo_spc = 0;                                                                //  current active curve
   zdialog_stuff(zd,"all",1);                                                    //  stuff default selection, all
   
   drawwin_dist = sd->drawarea;                                                  //  setup brightness distr. drawing area
   G_SIGNAL(drawwin_dist,"draw",brdist_drawgraph,RGBW);

   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,300,12);
   G_SIGNAL(drawwin_scale,"draw",brdist_drawscale,0);

   brightness = 0;                                                               //  neutral brightness
   contrast = 0;                                                                 //  neutral contrast

   wbalR = wbalG = wbalB = 1.0;                                                  //  neutral white balance
   wbtemp = 5000;                                                                //  neutral illumination temp.
   blackbodyRGB(wbtemp,tempR,tempG,tempB);                                       //  all are 1.0
   combR = combG = combB = 1.0;                                                  //  neutral RGB factors

   satlevel = 0;                                                                 //  neutral saturation

   areaemph = 0.5;                                                               //  even dark/bright area emphasis
   for (int ii = 0; ii < 256; ii++)
      emphcurve[ii] = 1;

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

   zdialog_resize(zd,300,450);
   zdialog_run(zd,combo_dialog_event,"save");                                    //  run dialog - parallel

   return;
}


//  dialog event and completion callback function

int combo_dialog_event(zdialog *zd, cchar *event)
{
   using namespace combo_names;

   void  combo_mousefunc();

   spldat      *sd = EFcombo.curves;
   float       c1, c2, xval, yval;
   float       bright0, dbrite, cont0, dcont;
   float       dx, dy;
   int         ii, nlo, nhi;
   int         Fapply = 0;
   
   if (strmatch(event,"done")) zd->zstat = 3;                                    //  apply and quit
   if (strmatch(event,"enter")) zd->zstat = 3;
   if (strmatch(event,"cancel")) zd->zstat = 4;
   if (strmatch(event,"escape")) zd->zstat = 4;                                  //  escape = cancel                    15.07
   
   if (strmatch(event,"apply")) Fapply = 1;                                      //  from script                        15.10

   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,"amplify",1);                                             //  neutral amplifier
      zdialog_stuff(zd,"brightness",0);                                          //  neutral brightness
      zdialog_stuff(zd,"contrast",0);                                            //  neutral contrast
      brightness = contrast = 0;

      wbalR = wbalG = wbalB = 1.0;                                               //  neutral white balance
      wbtemp = 5000;                                                             //  neutral illumination temp.
      zdialog_stuff(zd,"wbtemp",wbtemp);
      blackbodyRGB(wbtemp,tempR,tempG,tempB);                                    //  all are 1.0
      combR = combG = combB = 1.0;                                               //  neutral RGB factors

      satlevel = 0;                                                              //  neutral saturation
      zdialog_stuff(zd,"satlevel",0);

      areaemph = 0.5;                                                            //  even area emphasis
      zdialog_stuff(zd,"areaemph",0.5);
      for (int ii = 0; ii < 256; ii++)                                           //  flat curve
         emphcurve[ii] = 1;

      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->apx[ii][0] = sd->apy[ii][0] = 0.01;
         sd->apx[ii][1] = sd->apy[ii][1] = 0.50;
         sd->apx[ii][2] = sd->apy[ii][2] = 0.99;
         splcurve_generate(sd,ii);
         sd->fact[ii] = 0;                                                       //  bugfix                             16.02
      }

      combo_spc = 0;                                                             //  current curve is "all"
      sd->fact[0] = 1;
      zdialog_stuff(zd,"all",1);
      zdialog_stuff(zd,"red",0);                                                 //  bugfix                             16.02
      zdialog_stuff(zd,"green",0);
      zdialog_stuff(zd,"blue",0);
      edit_reset();                                                              //  restore initial image
      event = "update";                                                          //  trigger graph update
   }

   if (zd->zstat == 2)                                                           //  [prev] restore previous settings
   {
      zd->zstat = 0;                                                             //  keep dialog active
      edit_load_prev_widgets(&EFcombo);                                          //  15.10
      Fapply = 1;                                                                //  trigger apply event
   }

   if (zd->zstat)                                                                //  [done] or [cancel]
   {
      freeMouse();
      if (zd->zstat == 3 && CEF->Fmods) {                                        //  [done]
         edit_fullsize();                                                        //  get full size image
         signal_thread();
         edit_save_last_widgets(&EFcombo);                                       //  15.10
         edit_done(0);
      }
      else edit_cancel(0);                                                       //  [cancel] or [x]
      return 1;
   }

   if (strmatch(event,"load"))                                                   //  load all settings from a file
      edit_load_widgets(&EFcombo);

   if (strmatch(event,"save"))                                                   //  save all settings to a file
      edit_save_widgets(&EFcombo);

   if (strmatch(event,"click")) {                                                //  toggle mouse click input
      zdialog_fetch(zd,"click",ii);
      if (ii) takeMouse(combo_mousefunc,dragcursor);                             //  on: connect mouse function
      else freeMouse();                                                          //  off: free mouse
      return 1;
   }

   if (strmatch(event,"dist")) {                                                 //  distribution checkbox
      zdialog_fetch(zd,"dist",Fdist);
      event = "update";                                                          //  trigger graph update
   }

   if (strstr("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);
      combo_spc = 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;

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

      nlo = nhi = 0;                                                             //  count nodes in x-range 0.1-0.4     16.06
      for (ii = 0; ii < sd->nap[0]; ii++) {                                      //    and x-range 0.6-0.9
         dx = sd->apx[0][ii];
         if (dx > 0.1 && dx < 0.4) nlo++;
         if (dx > 0.6 && dx < 0.9) nhi++;
      }
      
      if (nlo == 0) {                                                            //  add node in low range if needed
         dx = 0.25;
         dy = sd->yval[0][250];
         splcurve_addnode(sd,0,dx,dy);
      }
      
      if (nhi == 0) {                                                            //  add node in high range if needed
         dx = 0.75;
         dy = sd->yval[0][750];
         splcurve_addnode(sd,0,dx,dy);
      }

      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.01 || dx >= 0.99) continue;
         dx = 0.5 - fabsf(dx - 0.5);                                             //  0 ... 0.5 ... 0
         dx = 0.5 * sqrtf(dx);                                                   //  0 ... 0.35 ... 0  (more rounded shape)
         dy = dx * dbrite;
         dy += sd->apy[0][ii];
         if (dy < 0) dy = 0;
         if (dy > 1) dy = 1;
         sd->apy[0][ii] = dy;
      }

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

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

      zdialog_stuff(zd,"all",1);                                                 //  active curve is "all"
      combo_spc = 0;
      sd->fact[0] = 1;
      for (ii = 1; ii < 4; ii++)
         sd->fact[ii] = 0;
      
      nlo = nhi = 0;                                                             //  count nodes in x-range 0.1-0.4     15.07
      for (ii = 0; ii < sd->nap[0]; ii++) {                                      //    and x-range 0.6-0.9
         dx = sd->apx[0][ii];
         if (dx > 0.1 && dx < 0.4) nlo++;
         if (dx > 0.6 && dx < 0.9) nhi++;
      }
      
      if (nlo == 0) {                                                            //  add node in low range if needed
         dx = 0.25;
         dy = sd->yval[0][250];
         splcurve_addnode(sd,0,dx,dy);
      }
      
      if (nhi == 0) {                                                            //  add node in high range if needed
         dx = 0.75;
         dy = sd->yval[0][750];
         splcurve_addnode(sd,0,dx,dy);
      }

      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.01 || dx >= 0.99) continue;
         if (dx < 0.5) dx = -0.25 + fabsf(dx - 0.25);                            //  0 ... -0.25 ... 0
         else dx = +0.25 - fabsf(dx - 0.75);                                     //  0 ... +0.25 ... 0
         dy = dx * 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 (Fdist) {                                                                  //  distribution enabled
      zdialog_fetch(zd,"red",RGBW[0]);                                           //  get graph color choice
      zdialog_fetch(zd,"green",RGBW[1]);
      zdialog_fetch(zd,"blue",RGBW[2]);
      zdialog_fetch(zd,"all",RGBW[3]);
      if (RGBW[3]) RGBW[0] = RGBW[1] = RGBW[2] = 1;
      RGBW[3] = 0;
   }
   else RGBW[0] = RGBW[1] = RGBW[2] = RGBW[3] = 0;

   if (strmatch(event,"update"))                                                 //  thread done or new color choice
      gtk_widget_queue_draw(drawwin_dist);                                       //  update distribution graph

   if (strmatch(event,"blendwidth")) Fapply = 1;                                 //  trigger apply event
   if (strstr("amplify brightness contrast",event)) Fapply = 1;
   if (strstr("satlevel areaemph wbtemp",event)) Fapply = 1;
   if (strmatch(event,"load")) Fapply = 1;

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

   zdialog_fetch(zd,"amplify",amplify);                                          //  get curve amplifier setting
   zdialog_fetch(zd,"brightness",brightness);                                    //  get brightness setting
   zdialog_fetch(zd,"contrast",contrast);                                        //  get contrast setting
   zdialog_fetch(zd,"satlevel",satlevel);                                        //  get saturation setting

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

   zdialog_fetch(zd,"areaemph",areaemph);                                        //  get dark/bright area emphasis

   if (areaemph < 0.5) {                                                         //  areaemph = 0 ... 0.5
      c1 = 1.2;                                                                  //  c1 = 1.2
      c2 = 2 * areaemph;                                                         //  c2 = 0 ... 1
   }
   else {                                                                        //  areaemph = 0.5 ... 1
      c1 = 2 * (1 - areaemph);                                                   //  c1 = 1 ... 0
      c2 = 1.2;                                                                  //  c2 = 1.2
   }

   for (ii = 0; ii < 256; ii++) {
      xval = ii / 256.0;                                                         //  xval = 0 ... 1
      yval = c1 + xval * (c2 - c1);                                              //  yval = c1 ... c2
      if (yval > 1.0) yval = 1.0;                                                //  limit to 1.0
      emphcurve[ii] = yval;
   }

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


//  this function is called when a curve is edited

void combo_curvedit(int spc)
{
   using namespace combo_names;
   signal_thread();
   return;
}


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

void combo_mousefunc()                                                           //  mouse function
{
   using namespace combo_names;

   int         px, py, dx, dy, ii;
   float       red, green, blue, rgbmean;
   float       *ppix;
   spldat      *sd = EFcombo.curves;                                             //  active curve, 1-4
   zdialog     *zd = EFcombo.zd;
   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 (red > 60 && green > 60 && blue > 60)                                      //  assume gray/white point         15.01
   {
      rgbmean = (red + green + blue) / 3.0;
      wbalR = rgbmean / red;                                                     //  = 1.0 if all RGB are equal
      wbalG = rgbmean / green;                                                   //  <1/>1 if RGB should be less/more
      wbalB = rgbmean / blue;
      signal_thread();                                                           //  trigger image update
   }

   else if (red < 50 && green < 50 && blue < 50)                                 //  assume this is a black point
   {
      zdialog_stuff(zd,"all",1);                                                 //  active curve is "all"
      combo_spc = 0;
      sd->fact[0] = 1;
      for (ii = 1; ii < 4; ii++)
         sd->fact[ii] = 0;

      sd->apx[0][0] = (red + green + blue) / 3.0 / 256.0;                        //  set low clipping level
      sd->apy[0][0] = 0.0;                                                       //    from black point pixel

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

      signal_thread();                                                           //  update image
   }

   return;
}


//  combo thread function

void * combo_thread(void *arg)
{
   using namespace combo_names;

   void * combo_wthread(void *);

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

      combR = wbalR * tempR / 256.0;                                             //  gray standard based on clicked pixel
      combG = wbalG * tempG / 256.0;                                             //    colors and illumination temp.
      combB = wbalB * tempB / 256.0;                                             //      <1/>1 if RGB should be less/more

      for (int ii = 0; ii < NWT; ii++)                                           //  start worker threads
         start_wthread(combo_wthread,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

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

      Fpaint2();                                                                 //  update window

      if (CEF->thread_status > 1) continue;                                      //  continue working   Mint Lockup     15.12.1

      if (CEF->zd) {
         zd_thread = CEF->zd;                                                    //  signal dialog to update graph
         zd_thread_event = "update";
      }
      else zd_thread = 0;
   }

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


void * combo_wthread(void *arg)                                                  //  worker thread function
{
   using namespace combo_names;

   int         index = *((int *) arg);
   int         ii, dist = 0, px, py;
   float       *pix1, *pix3;
   float       red1, green1, blue1, maxrgb;
   float       red3, green3, blue3;
   float       pixbrite, F1, F2;
   float       coeff = 1000.0 / 256.0;
   float       dold, dnew, ff;
   spldat      *sd = EFcombo.curves;
   
   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

      red1 = pix1[0];
      green1 = pix1[1];
      blue1 = pix1[2];

      //  apply white balance and temperature color shift

      red3 = combR * red1;                                                       //  <1/>1 if RGB should be less/more
      green3 = combG * green1;
      blue3 = combB * blue1;

      //  apply saturation color shift

      if (satlevel != 0) {
         pixbrite = 0.333 * (red3 + green3 + blue3);                             //  pixel brightness, 0 to 255.9
         red3 = red3 + satlevel * (red3 - pixbrite);                             //  satlevel is -1 ... +1
         green3 = green3 + satlevel * (green3 - pixbrite);
         blue3 = blue3 + satlevel * (blue3 - pixbrite);
      }

      if (red3 < 0) red3 = 0;                                                    //  stop underflow
      if (green3 < 0) green3 = 0;
      if (blue3 < 0) blue3 = 0;

      maxrgb = red3;                                                             //  stop overflow
      if (green3 > maxrgb) maxrgb = green3;
      if (blue3 > maxrgb) maxrgb = blue3;
      if (maxrgb > 255.9) {
         red3 = red3 * 255.9 / maxrgb;
         green3 = green3 * 255.9 / maxrgb;
         blue3 = blue3 * 255.9 / maxrgb;
      }

      //  apply dark/bright area emphasis curve for color changes

      if (areaemph != 0.5) {
         maxrgb = red1;                                                          //  use original colors
         if (green1 > maxrgb) maxrgb = green1;
         if (blue1 > maxrgb) maxrgb = blue1;
         F1 = emphcurve[int(maxrgb)];                                            //  0 ... 1   neutral is flat curve = 1
         F2 = 1.0 - F1;
         red3 = F1 * red3 + F2 * red1;
         green3 = F1 * green3 + F2 * green1;
         blue3 = F1 * blue3 + F2 * blue1;
      }

      //  apply brightness/contrast curve

      pixbrite = red3;                                                           //  use max. RGB value
      if (green3 > pixbrite) pixbrite = green3;
      if (blue3 > pixbrite) pixbrite = blue3;
      
      ii = coeff * pixbrite;                                                     //  curve index 0-999

      if (amplify == 1.0)                                                        //  amplifier is neutral
      {
         ff = 255.9 * sd->yval[0][ii];                                           //  curve "all" adjustment
         red3 = ff * red3 / pixbrite;                                            //  projected on each RGB color
         green3 = ff * green3 / pixbrite;
         blue3 = ff * blue3 / pixbrite;
      }
      else                                                                       //  use amplifier
      {
         ff = 0.001 * ii;
         ff = ff + amplify * (sd->yval[0][ii] - ff);                             //  amplify (curve - baseline) difference
         if (ff < 0) ff = 0;
         red3 = 255.9 * ff * red3 / pixbrite;
         green3 = 255.9 * ff * green3 / pixbrite;
         blue3 = 255.9 * ff * blue3 / pixbrite;
      }

      ii = coeff * red3;                                                         //  add additional RGB adjustments
      if (ii < 0) ii = 0;
      if (ii > 999) ii = 999;
      red3 = 255.9 * sd->yval[1][ii];                                            //  output brightness, 0-255.9

      ii = coeff * green3;
      if (ii < 0) ii = 0;
      if (ii > 999) ii = 999;
      green3 = 255.9 * sd->yval[2][ii];

      ii = coeff * blue3;
      if (ii < 0) ii = 0;
      if (ii > 999) ii = 999;
      blue3 = 255.9 * sd->yval[3][ii];

      if (red3 < 0) red3 = 0;                                                    //  stop underflow
      if (green3 < 0) green3 = 0;
      if (blue3 < 0) blue3 = 0;

      maxrgb = red3;                                                             //  stop overflows
      if (green3 > maxrgb) maxrgb = green3;
      if (blue3 > maxrgb) maxrgb = blue3;
      if (maxrgb > 255.9) {
         red3 = red3 * 255.9 / maxrgb;
         green3 = green3 * 255.9 / maxrgb;
         blue3 = blue3 * 255.9 / maxrgb;
      }

      //  select area edge blending

      if (sa_stat == 3 && dist < sa_blend) {
///      dnew = 1.0 * dist / sa_blend;
         dnew = sa_blendfunc(dist);                                              //  16.08
         dold = 1.0 - dnew;
         red3 = dnew * red3 + dold * red1;
         green3 = dnew * green3 + dold * green1;
         blue3 = dnew * blue3 + dold * blue1;
      }

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

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


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

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


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

//  adjust brightness distribution by flattening and/or expanding range

namespace britedist_names
{
   int      ww, hh;
   float    LC, HC;                                                              //  low, high cutoff levels
   float    LF, MF, HF;                                                          //  low, mid, high flatten parms
   float    LS, MS, HS;                                                          //  low, mid, high stretch parms
   float    BB[1000];                                                            //  adjusted B for input B 0-999

   int    dialog_event(zdialog* zd, cchar *event);
   void   compute_BB();
   void * thread(void *);
   void * wthread(void *);

   editfunc    EFbrightdist;
}


//  menu function

void m_britedist(GtkWidget *, cchar *menu)                                       //  15.08
{
   using namespace britedist_names;

   cchar  *title = ZTX("Adjust Brightness Distribution");

   F1_help_topic = "edit_brightness";

   EFbrightdist.menuname = menu;
   EFbrightdist.menufunc = m_britedist;
   EFbrightdist.funcname = "brightdist";
   EFbrightdist.FprevReq = 1;                                                    //  preview
   EFbrightdist.Farea = 2;                                                       //  select area usable
   EFbrightdist.Frestart = 1;                                                    //  restart allowed
   EFbrightdist.Fscript = 1;                                                     //  scripting supported                15.10
   EFbrightdist.threadfunc = thread;
   if (! edit_setup(EFbrightdist)) return;                                       //  setup edit

/***
          __________________________________________
         |      Adjust Brightness Distribution      |
         |                                          |
         | Low Cutoff   ==========[]==============  |
         | High Cutoff  ==============[]==========  |
         | Low Flatten  =========[]===============  |
         | Mid Flatten  ============[]============  |
         | High Flatten ==============[]==========  |
         | Low Stretch  ================[]========  |
         | Mid Stretch  =============[]===========  |
         | High Stretch =========[]===============  |
         |                                          |
         |                  [Reset] [Done] [Cancel] |
         |__________________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,Breset,Bdone,Bcancel,null);
   EFbrightdist.zd = zd;

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

   zdialog_add_widget(zd,"label","labLC","vb1",ZTX("Low Cutoff"));
   zdialog_add_widget(zd,"label","labHC","vb1",ZTX("High Cutoff"));
   zdialog_add_widget(zd,"label","labLF","vb1",ZTX("Low Flatten"));
   zdialog_add_widget(zd,"label","labMF","vb1",ZTX("Mid Flatten"));
   zdialog_add_widget(zd,"label","labHF","vb1",ZTX("High Flatten"));
   zdialog_add_widget(zd,"label","labLS","vb1",ZTX("Low Stretch"));
   zdialog_add_widget(zd,"label","labMS","vb1",ZTX("Mid Stretch"));
   zdialog_add_widget(zd,"label","labHS","vb1",ZTX("High Stretch"));

   zdialog_add_widget(zd,"hscale","LC","vb2","0|1.0|0.002|0","expand");
   zdialog_add_widget(zd,"hscale","HC","vb2","0|1.0|0.002|0","expand");
   zdialog_add_widget(zd,"hscale","LF","vb2","0|1.0|0.002|0","expand");
   zdialog_add_widget(zd,"hscale","MF","vb2","0|1.0|0.002|0","expand");
   zdialog_add_widget(zd,"hscale","HF","vb2","0|1.0|0.002|0","expand");
   zdialog_add_widget(zd,"hscale","LS","vb2","0|1.0|0.002|0","expand");
   zdialog_add_widget(zd,"hscale","MS","vb2","0|1.0|0.002|0","expand");
   zdialog_add_widget(zd,"hscale","HS","vb2","0|1.0|0.002|0","expand");

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

   m_show_brdist(0,0);                                                           //  popup brightness histogram
   
   LC = HC = LF = MF = HF = LS = MS = HS = 0.0;
   compute_BB();
   
   return;
}


//  dialog event and completion function

int britedist_names::dialog_event(zdialog *zd, cchar *event)
{
   using namespace britedist_names;
   
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  apply and quit
   if (strmatch(event,"enter")) zd->zstat = 2;
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  cancel
   if (strmatch(event,"escape")) zd->zstat = 4;                                  //  escape = cancel                    15.07

   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();                                                           //  get full size image
      compute_BB();
      signal_thread();
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  reset
         zd->zstat = 0;                                                          //  keep dialog active
         LC = HC = LF = MF = HF = LS = MS = HS = 0.0;
         zdialog_stuff(zd,"LC",LC);
         zdialog_stuff(zd,"HC",HC);
         zdialog_stuff(zd,"LF",LF);
         zdialog_stuff(zd,"MF",MF);
         zdialog_stuff(zd,"HF",HF);
         zdialog_stuff(zd,"LS",LS);
         zdialog_stuff(zd,"MS",MS);
         zdialog_stuff(zd,"HS",HS);
         compute_BB();
         signal_thread();
      }
      else if (zd->zstat == 2 && CEF->Fmods) {                                   //  done                               16.06
         edit_fullsize();                                                        //  get full size image
         compute_BB();
         signal_thread();
         m_show_brdist(0,"kill");                                                //  kill distribution graph
         edit_done(0);                                                           //  commit edit
         return 1;
      }
      else {
         edit_cancel(0);                                                         //  cancel - discard edit
         m_show_brdist(0,"kill");                                                //  kill distribution graph
         return 1;
      }
   }

   if (strmatch(event,"blendwidth"))                                             //  select area blendwidth change
      signal_thread();
   
   if (strstr("LC HC LF MF HF LS MS HS apply",event))
   {
      zdialog_fetch(zd,"LC",LC);
      zdialog_fetch(zd,"HC",HC);
      zdialog_fetch(zd,"LF",LF);
      zdialog_fetch(zd,"MF",MF);
      zdialog_fetch(zd,"HF",HF);
      zdialog_fetch(zd,"LS",LS);
      zdialog_fetch(zd,"MS",MS);
      zdialog_fetch(zd,"HS",HS);
      compute_BB();
      signal_thread();
   }

   wait_thread_idle();                                                           //  no overlap window update and threads
   Fpaintnow();
   return 1;
}


//  compute flattened brightness levels for preview size or full size image
//  FB[B] = flattened brightness level for brightness B, scaled 0-1000

void britedist_names::compute_BB()
{
   using namespace britedist_names;

   int      ii, npix, py, px, iB;
   float    *pix1;
   float    B, LC2, HC2;
   float    FB[1000];
   float    LF2, MF2, HF2, LS2, MS2, HS2;
   float    LFB, MFB, HFB, LSB, MSB, HSB;
   float    LFW, MFW, HFW, LSW, MSW, HSW, TWB;

   ww = E1pxm->ww;
   hh = E1pxm->hh;

   for (ii = 0; ii < 1000; ii++)                                                 //  clear brightness distribution data
      FB[ii] = 0;

   if (sa_stat == 3)                                                             //  process selected area
   {
      for (ii = npix = 0; ii < ww * hh; ii++)
      {
         if (! sa_pixmap[ii]) continue;
         py = ii / ww;
         px = ii - py * ww;
         pix1 = PXMpix(E1pxm,px,py);
         B = 1000.0 * (pix1[0] + pix1[1] + pix1[2]) / 768.0;
         FB[int(B)]++;
         npix++;
      }

      for (ii = 1; ii < 1000; ii++)                                              //  cumulative brightness distribution
         FB[ii] += FB[ii-1];                                                     //   0 ... npix

      for (ii = 0; ii < 1000; ii++)
         FB[ii] = FB[ii] / npix * 999.0;                                         //  flattened brightness level
   }

   else                                                                          //  process whole image
   {
      for (py = 0; py < hh; py++)                                                //  compute brightness distribution
      for (px = 0; px < ww; px++)
      {
         pix1 = PXMpix(E1pxm,px,py);
         B = 1000.0 * (pix1[0] + pix1[1] + pix1[2]) / 768.0;
         FB[int(B)]++;
      }

      for (ii = 1; ii < 1000; ii++)                                              //  cumulative brightness distribution
         FB[ii] += FB[ii-1];                                                     //   0 ... (ww * hh)
   
      for (ii = 0; ii < 1000; ii++)
         FB[ii] = FB[ii] / (ww * hh) * 999.0;                                    //  flattened brightness level
   }

   LC2 = 500 * LC;                                                               //  low cutoff, 0 ... 500
   HC2 = 1000 - 500 * HC;                                                        //  high cutoff, 1000 ... 500

   for (iB = 0; iB < 1000; iB++)                                                 //  loop brightness 0 - 1000   
   {
      B = iB;

      if (LC2 > 0 || HC2 < 1000) {                                               //  stretch to cutoff limits
         if (B < LC2) B = 0;
         else if (B > HC2) B = 999;
         else B = 1000.0 * (B - LC2) / (HC2 - LC2);
      }
      
      LF2 = LF * (1000 - B) / 1000;                                              //  low flatten  LF ... 0
      LF2 = LF2 * LF2;
      LFB = LF2 * FB[iB] + (1.0 - LF2) * B;
      
      LS2 = LS * (1000 - B) / 1000;                                              //  low stretch  LS ... 0
      LS2 = LS2 * LS2;
      LSB = B * (1 - LS2);
      
      MF2 = MF * (500 - fabsf(B - 500)) / 500;                                   //  mid flatten  0 ... MF ... 0
      MF2 = sqrtf(MF2);
      MFB = MF2 * FB[iB] + (1.0 - MF2) * B;

      MS2 = MS * (B - 500) / 500;                                                //  mid stretch  -MS ... 0 ... MS
      MSB = B * (1 + 0.5 * MS2);
      
      HF2 = HF * B / 1000;                                                       //  high flatten  0 ... HF
      HF2 = HF2 * HF2;
      HFB = HF2 * FB[iB] + (1.0 - HF2) * B;

      HS2 = HS * B / 1000;                                                       //  high stretch  0 ... HS
      HS2 = HS2 * HS2;
      HSB = B * (1 + HS2);
      
      LFW = fabsf(B - LFB) / (B + 1);                                            //  weight of each component
      LSW = fabsf(B - LSB) / (B + 1);
      MFW = fabsf(B - MFB) / (B + 1);
      MSW = fabsf(B - MSB) / (B + 1);
      HFW = fabsf(B - HFB) / (B + 1);
      HSW = fabsf(B - HSB) / (B + 1);
      
      TWB = LFW + LSW + MFW + MSW + HFW + HSW;                                   //  add weighted components
      if (TWB == 0) BB[iB] = B;
      else BB[iB] = (LFW * LFB + LSW * LSB
                   + MFW * MFB + MSW * MSB 
                   + HFW * HFB + HSW * HSB) / TWB;
   }
   
   return;
}


//  adjust brightness distribution thread function

void * britedist_names::thread(void *)
{
   using namespace britedist_names;

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

      for (int ii = 0; ii < NWT; ii++)                                           //  start worker thread per processor core
         start_wthread(wthread,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

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

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


//  worker thread for each CPU processor core

void * britedist_names::wthread(void *arg)
{
   using namespace britedist_names;

   int         index = *((int *) (arg));
   int         iB, px, py, ii, dist = 0;
   float       B, *pix1, *pix3;
   float       dold, dnew, cmax;
   float       red1, green1, blue1, red3, green3, blue3;
   
   for (py = index; py < E1pxm->hh; py += NWT)                                   //  flatten brightness distribution
   for (px = 0; px < E1pxm->ww; px++)
   {
      if (sa_stat == 3) {                                                        //  select area active
         ii = py * E1pxm->ww + px;
         dist = sa_pixmap[ii];                                                   //  distance from edge
         if (! dist) continue;                                                   //  outside pixel
      }

      pix1 = PXMpix(E1pxm,px,py);                                                //  input pixel
      pix3 = PXMpix(E3pxm,px,py);                                                //  output pixel
      
      B = (pix1[0] + pix1[1] + pix1[2]) / 768.0 * 1000.0;                        //  pixel brightness scaled 0-1000
      iB = int(B);
      if (B > 0) B = BB[iB] / B;
      
      red1 = pix1[0];
      green1 = pix1[1];
      blue1 = pix1[2];
      
      red3 = B * red1;
      green3 = B * green1;
      blue3 = B * blue1;

      if (sa_stat == 3 && dist < sa_blend) {                                     //  select area is active,
///      dnew = 1.0 * dist / sa_blend;                                           //    blend changes over sa_blend
         dnew = sa_blendfunc(dist);                                              //  16.08
         dold = 1.0 - dnew;
         red3 = dnew * red3 + dold * red1;
         green3 = dnew * green3 + dold * green1;
         blue3 = dnew * blue3 + dold * blue1;
      }

      cmax = red3;                                                               //  stop overflow, keep color balance
      if (green3 > cmax) cmax = green3;
      if (blue3 > cmax) cmax = blue3;
      if (cmax > 255.9) {
         cmax = 255.9 / cmax;
         red3 = red3 * cmax;
         green3 = green3 * cmax;
         blue3 = blue3 * cmax;
      }

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

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


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

//  flatten brightness distribution based on the distribution of nearby zones

namespace zonal_flatten_names
{
   int      Zinit;                                 //  zone initialization needed
   float    flatten;                               //  flatten amount, 0 - 100
   float    deband1, deband2;                      //  deband dark, bright, 0 - 100
   int      Iww, Ihh;                              //  image dimensions
   int      NZ;                                    //  no. of image zones
   int      pNZ;                                   //  prior image zones
   int      Zsize, Zrows, Zcols;                   //  zone parameters
   float    *Br;                                   //  Br[py][px]  pixel brightness
   int      *Zxlo, *Zylo, *Zxhi, *Zyhi;            //  Zxlo[ii] etc.  zone ii pixel range
   int      *Zcen;                                 //  Zcen[ii][2]  zone ii center (py,px)
   int16    *Zn;                                   //  Zn[py][px][9]  9 nearest zones for pixel: 0-200 (-1 = none)      16.02
   uint8    *Zw;                                   //  Zw[py][px][9]  zone weights for pixel: 0-100 = 1.0
   float    *Zff;                                  //  Zff[ii][1000]  zone ii flatten factors

   editfunc    EFzonalflat;

   int    dialog_event(zdialog* zd, cchar *event);
   void   doflatten();
   void   calczones();
   void   initzones();
   void * thread(void *);
   void * wthread(void *);
}


//  menu function

void m_zonal_flatten(GtkWidget *, cchar *menu)                                   //  15.04
{
   using namespace zonal_flatten_names;

   cchar  *title = ZTX("Zonal Flatten Brightness");

   F1_help_topic = "zonal_flatten";

   Iww = Fpxb->ww;
   Ihh = Fpxb->hh;

   if (Iww * Ihh > 227 * MEGA) {
      zmessageACK(Mwin,"image too big");                                         //  16.03
      edit_cancel(0);
      return;
   }

   EFzonalflat.menuname = menu;
   EFzonalflat.menufunc = m_zonal_flatten;
   EFzonalflat.funcname = "zonal-flatten";
   EFzonalflat.FprevReq = 1;                                                     //  use preview image
   EFzonalflat.Farea = 2;                                                        //  select area usable
   EFzonalflat.Frestart = 1;                                                     //  restartable                        15.05
   EFzonalflat.FusePL = 1;                                                       //  use with paint/lever edits OK
   EFzonalflat.Fscript = 1;                                                      //  scripting supported                15.10
   EFzonalflat.threadfunc = thread;
   if (! edit_setup(EFzonalflat)) return;                                        //  setup edit

   Iww = E0pxm->ww;
   Ihh = E0pxm->hh;

   if (Iww * Ihh > 227 * MEGA) {
      zmessageACK(Mwin,"image too big");                                         //  16.03
      edit_cancel(0);
      return;
   }

/***
          ______________________________________
         |      Zonal Flatten Brightness        |
         |                                      |
         | Zones  [ 123|-+]   [Apply]           |
         | Flatten  =========[]============ NN  |
         | Deband Dark =========[]========= NN  |
         | Deband Bright ==========[]====== NN  |
         |                                      |
         |                     [Done] [Cancel]  |
         |______________________________________|

***/

   zdialog *zd = zdialog_new(title,Mwin,Bdone,Bcancel,null);
   EFzonalflat.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labreg","hb1",ZTX("Zones"),"space=5");
   zdialog_add_widget(zd,"spin","zones","hb1","20|999|1|100");                   //  16.02
   zdialog_add_widget(zd,"button","apply","hb1",Bapply,"space=10");
   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"label","labflatten","hb2",Bflatten,"space=5");
   zdialog_add_widget(zd,"hscale","flatten","hb2","0|100|1|0","expand");
   zdialog_add_widget(zd,"hbox","hb3","dialog");
   zdialog_add_widget(zd,"label","labdeband1","hb3",ZTX("Deband Dark"),"space=5");
   zdialog_add_widget(zd,"hscale","deband1","hb3","0|100|1|0","expand");
   zdialog_add_widget(zd,"hbox","hb4","dialog");
   zdialog_add_widget(zd,"label","labdeband2","hb4",ZTX("Deband Bright"),"space=5");
   zdialog_add_widget(zd,"hscale","deband2","hb4","0|100|1|0","expand");

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

   NZ = pNZ = 40;                                                                //  default zone count
   calczones();                                                                  //  adjust to fit image
   flatten = deband1 = deband2 = 0;                                              //  dialog controls = neutral
   Zinit = 1;                                                                    //  zone initialization needed
   Br = 0;                                                                       //  no memory allocated
   return;
}


//  dialog event and completion function

int zonal_flatten_names::dialog_event(zdialog *zd, cchar *event)
{
   using namespace zonal_flatten_names;
   
   if (strmatch(event,"done")) zd->zstat = 1;                                    //  apply and quit
   if (strmatch(event,"enter")) zd->zstat = 1;
   if (strmatch(event,"cancel")) zd->zstat = 2;                                  //  cancel
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (strmatch(event,"fullsize")) {                                             //  from select area
      edit_fullsize();
      Zinit = 1;
      doflatten();
      return 1;
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  done
         edit_fullsize();                                                        //  get full size image
         Zinit = 1;                                                              //  recalculate zones
         doflatten();                                                            //  flatten full image
         edit_done(0);                                                           //  commit edit
      }
      else edit_cancel(0);                                                       //  discard edit

      if (Br) {
         zfree(Br);                                                              //  free memory
         zfree(Zn);
         zfree(Zw);
         zfree(Zxlo);
         zfree(Zylo);
         zfree(Zxhi);
         zfree(Zyhi);
         zfree(Zcen);
         zfree(Zff);
         Br = 0;
      }

      return 1;
   }

   if (strmatch(event,"apply")) {                                                //  [apply]  (also from script)        15.10
      dialog_event(zd,"zones");
      dialog_event(zd,"flatten");
      dialog_event(zd,"deband1");
      dialog_event(zd,"deband2");
      doflatten();
   }

   if (strmatch(event,"blendwidth"))                                             //  select area blendwidth change
      doflatten();

   if (strmatch(event,"zones")) {                                                //  get zone count input
      zdialog_fetch(zd,"zones",NZ);
      if (NZ == pNZ) return 1;
      calczones();                                                               //  adjust to fit image
      zdialog_stuff(zd,"zones",NZ);                                              //  update dialog
      Zinit = 1;                                                                 //  zone initialization needed
   }

   if (strmatch(event,"flatten")) {
      zdialog_fetch(zd,"flatten",flatten);                                       //  get slider values
      doflatten();
   }

   if (strmatch(event,"deband1")) {
      zdialog_fetch(zd,"deband1",deband1);
      doflatten();
   }

   if (strmatch(event,"deband2")) {
      zdialog_fetch(zd,"deband2",deband2);
      doflatten();
   }

   return 1;
}


//  perform the flatten calculations and update the image

void zonal_flatten_names::doflatten()
{
   if (flatten > 0) {
      signal_thread();
      wait_thread_idle();                                                        //  no overlap window update and threads
      Fpaintnow();
   }
   else edit_reset();
   return;
}


//  recalculate zone count based on what fits the image dimensions
//  done only when the user zone count input changes
//  outputs: NZ, Zrows, Zcols

void zonal_flatten_names::calczones()
{
   int      gNZ, dNZ;

   Iww = E1pxm->ww;                                                              //  image dimensions
   Ihh = E1pxm->hh;

   gNZ = NZ;                                                                     //  new zone count goal
   dNZ = NZ - pNZ;                                                               //  direction of change

   while (true)
   {
      Zsize = sqrtf(Iww * Ihh / gNZ);                                            //  approx. zone size
      Zrows = Ihh / Zsize + 0.5;                                                 //  get appropriate rows and cols
      Zcols = Iww / Zsize + 0.5;
      NZ = Zrows * Zcols;                                                        //  NZ matching rows x cols

      if (dNZ > 0 && NZ <= pNZ) {                                                //  no increase, try again
         if (NZ >= 999) break;                                                   //  16.02
         gNZ++;
         continue;
      }

      if (dNZ < 0 && NZ >= pNZ) {                                                //  no decrease, try again
         if (NZ <= 20) break;
         gNZ--;
         continue;
      }

      if (dNZ == 0) break;
      if (dNZ > 0 && NZ > pNZ) break;
      if (dNZ < 0 && NZ < pNZ) break;
   }

   pNZ = NZ;                                                                     //  final zone count
   dNZ = 0;
   return;
}


//  build up the zones data when NZ or the image size changes
//  (preview or full size image)

void zonal_flatten_names::initzones()
{
   int      px, py, cx, cy;
   int      rx, ii, jj, kk;
   int      zww, zhh, row, col;
   float    *pix1, bright;
   float    weight[9], sumweight;
   float    D, Dthresh;

   if (Br) {
      zfree(Br);                                                                 //  free memory
      zfree(Zn);
      zfree(Zw);
      zfree(Zxlo);
      zfree(Zylo);
      zfree(Zxhi);
      zfree(Zyhi);
      zfree(Zcen);
      zfree(Zff);
      Br = 0;
   }

   Iww = E1pxm->ww;                                                              //  image dimensions
   Ihh = E1pxm->hh;

   Br = (float *) zmalloc(Iww * Ihh * sizeof(float));                            //  allocate memory
   Zn = (int16 *) zmalloc(Iww * Ihh * 9 * sizeof(int16));                        //  16.02
   Zw = (uint8 *) zmalloc(Iww * Ihh * 9 * sizeof(uint8));
   Zxlo = (int *) zmalloc(NZ * sizeof(int));
   Zylo = (int *) zmalloc(NZ * sizeof(int));
   Zxhi = (int *) zmalloc(NZ * sizeof(int));
   Zyhi = (int *) zmalloc(NZ * sizeof(int));
   Zcen = (int *) zmalloc(NZ * 2 * sizeof(int));
   Zff = (float *) zmalloc(NZ * 1000 * sizeof(float));

   for (py = 0; py < Ihh; py++)                                                  //  get initial pixel brightness levels
   for (px = 0; px < Iww; px++)
   {
      pix1 = PXMpix(E1pxm,px,py);
      bright = pixbright(pix1);
      ii = py * Iww + px;
      Br[ii] = bright;
   }

   zww = Iww / Zcols;                                                            //  actual zone size
   zhh = Ihh / Zrows;

   for (row = 0; row < Zrows; row++)
   for (col = 0; col < Zcols; col++)                                             //  set low/high bounds per zone
   {
      ii = row * Zcols + col;
      Zxlo[ii] = col * zww;
      Zylo[ii] = row * zhh;
      Zxhi[ii] = Zxlo[ii] + zww;
      Zyhi[ii] = Zylo[ii] + zhh;
      Zcen[2*ii] = Zylo[ii] + zhh/2;
      Zcen[2*ii+1] = Zxlo[ii] + zww/2;
   }

   for (ii = 0; ii < NZ; ii++)                                                   //  compute brightness distributiion
   {                                                                             //    for each zone
      for (jj = 0; jj < 1000; jj++)
         Zff[1000*ii+jj] = 0;

      for (py = Zylo[ii]; py < Zyhi[ii]; py++)                                   //  brightness distribution
      for (px = Zxlo[ii]; px < Zxhi[ii]; px++)
      {
         pix1 = PXMpix(E1pxm,px,py);
         kk = pixbright(pix1);
         if (kk > 255) kk = 255;                                                 //  15.12
         bright = 3.906 * kk;                                                    //  1000 / 256 * kk
         Zff[1000*ii+int(bright)]++;
      }

      for (jj = 1; jj < 1000; jj++)                                              //  cumulative brightness distribution
         Zff[1000*ii+jj] += Zff[1000*ii+jj-1];

      for (jj = 0; jj < 1000; jj++)                                              //  multiplier per brightness level
         Zff[1000*ii+jj] = Zff[1000*ii+jj] / (zww*zhh) * 1000 / (jj+1);          //    to make distribution flat
   }

   for (py = 0; py < Ihh; py++)                                                  //  set 9 nearest zones per pixel
   for (px = 0; px < Iww; px++)
   {
      rx = (py * Iww + px) * 9;                                                  //  index for 9 nearest zones to px/py

      row = py / zhh;                                                            //  zone containing pixel
      col = px / zww;

      ii = 0;
      for (jj = row-1; jj <= row+1; jj++)                                        //  loop 3x3 surrounding zones
      for (kk = col-1; kk <= col+1; kk++)
      {
         if (jj >= 0 && jj < Zrows && kk >= 0 && kk < Zcols)                     //  zone is not off the edge
            Zn[rx+ii] = jj * Zcols + kk;
         else Zn[rx+ii] = -1;                                                    //  edge row/col: missing neighbor     16.02
         ii++;
      }
   }

   if (zww < zhh)                                                                //  pixel to zone distance threshold
      Dthresh = 1.5 * zww;                                                       //  area influence = 0 beyond this distance
   else Dthresh = 1.5 * zhh;

   for (py = 0; py < Ihh; py++)                                                  //  set zone weights per pixel
   for (px = 0; px < Iww; px++)
   {
      rx = (py * Iww + px) * 9;                                                  //  index for 9 nearest zones to px/py

      for (ii = 0; ii < 9; ii++)                                                 //  distance to each zone center
      {
         jj = Zn[rx+ii];
         if (jj == -1) {                                                         //  missign zone                       16.02
            weight[ii] = 0;                                                      //  weight = 0
            continue;
         }
         cy = Zcen[2*jj];
         cx = Zcen[2*jj+1];
         D = sqrtf((py-cy)*(py-cy) + (px-cx)*(px-cx));                           //  distance from pixel to zone center
         D = D / Dthresh;
         if (D > 1) D = 1;                                                       //  zone influence reaches zero at Dthresh
         weight[ii] = 1.0 - D;
      }

      sumweight = 0;
      for (ii = 0; ii < 9; ii++)                                                 //  zone weights based on distance
         sumweight += weight[ii];

      for (ii = 0; ii < 9; ii++)                                                 //  normalize weights, sum = 1.0
         Zw[rx+ii] = 100 * weight[ii] / sumweight;                               //  0-100 = 1.0
   }

   return;
}


//  adjust brightness distribution thread function

void * zonal_flatten_names::thread(void *)
{
   using namespace zonal_flatten_names;

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

      if (Zinit) initzones();                                                    //  reinitialize zones
      Zinit = 0;

      for (int ii = 0; ii < NWT; ii++)                                           //  start worker thread per processor core
         start_wthread(wthread,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

      if (! flatten) CEF->Fmods = 0;                                             //  no modification
      else {
         CEF->Fmods++;                                                           //  image modified, not saved
         CEF->Fsaved = 0;
      }
   }

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


//  worker thread for each CPU processor core

void * zonal_flatten_names::wthread(void *arg)
{
   using namespace zonal_flatten_names;

   int         index = *((int *) (arg));
   int         px, py, rx, ii, jj, dist = 0;
   float       *pix1, *pix3;
   float       fnew1, fnew2, fnew, fold;
   float       dold, dnew, cmax;
   float       red1, green1, blue1, red3, green3, blue3;
   float       FF, weight, bright;

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

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

      red1 = pix1[0];
      green1 = pix1[1];
      blue1 = pix1[2];

      bright = red1 + green1 + blue1;                                            //  input pixel brightness 0-767.9
      bright *= 1000.0 / 768.0;                                                  //  0-999.9
      
      rx = (py * Iww + px) * 9;                                                  //  index for 9 nearest zones to px/py

      FF = 0;
      for (ii = 0; ii < 9; ii++) {                                               //  loop 9 nearest zones
         weight = Zw[rx+ii];
         if (weight > 0) {                                                       //  0-100 = 1.0
            jj = Zn[rx+ii];
            FF += 0.01 * weight * Zff[1000*jj+int(bright)];                      //  sum weight * flatten factor
         }
      }

      red3 = FF * red1;                                                          //  fully flattened brightness
      green3 = FF * green1;
      blue3 = FF * blue1;
      
      fnew1 = 1 - 0.01 * deband1 * 0.001 * (1000 - bright);                      //  attenuate dark pixels              15.05
      fnew2 = 1 - 0.01 * deband2 * 0.001 * bright;                               //  attenuate bright pixels            15.05
      fnew = fnew1 * fnew2;

      fnew = fnew * 0.01 * flatten;                                              //  how much to flatten, 0 to 1
      fold = 1.0 - fnew;                                                         //  how much to retain, 1 to 0

      red3 = fnew * red3 + fold * red1;                                          //  blend new and old brightness
      green3 = fnew * green3 + fold * green1;
      blue3 = fnew * blue3 + fold * blue1;

      if (sa_stat == 3 && dist < sa_blend) {                                     //  select area is active,
///      dnew = 1.0 * dist / sa_blend;                                           //    blend changes over sa_blend
         dnew = sa_blendfunc(dist);                                              //  16.08
         dold = 1.0 - dnew;
         red3 = dnew * red3 + dold * red1;
         green3 = dnew * green3 + dold * green1;
         blue3 = dnew * blue3 + dold * blue1;
      }

      cmax = red3;                                                               //  stop overflow, keep color balance
      if (green3 > cmax) cmax = green3;
      if (blue3 > cmax) cmax = blue3;
      if (cmax > 255.9) {
         cmax = 255.9 / cmax;
         red3 = red3 * cmax;
         green3 = green3 * cmax;
         blue3 = blue3 * cmax;
      }

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

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


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

   Image Tone Mapping function
   enhance local contrast as opposed to overall contrast

   methodology:
   get brightness gradients for each pixel in 4 directions: SE SW NE NW
   amplify gradients using the edit curve (x-axis range 0-max. gradient)
   integrate 4 new brightness surfaces from the amplified gradients:
     - pixel brightness = prior pixel brightness + amplified gradient
     - the Amplify control varies amplification from zero to overamplified
   new pixel brightness = average from 4 calculated brightness surfaces

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


namespace tonemap_names
{
   float       *brmap1, *brmap2;
   float       *brmap4[4];
   int         contrast99;
   float       amplify;
   int         ww, hh;
   editfunc    EFtonemap;

   void   Tmap_initz(zdialog *zd);
   int    Tmap_dialog_event(zdialog *zd, cchar *event);
   void   Tmap_curvedit(int);
   void * Tmap_thread(void *);
   void * Tmap_wthread1(void *arg);
   void * Tmap_wthread2(void *arg);
   void * Tmap_wthread3(void *arg);

}


void m_tonemap(GtkWidget *, cchar *menu)
{
   using namespace tonemap_names;

   F1_help_topic = "tone_mapping";

   cchar    *title = ZTX("Tone Mapping");
   
   EFtonemap.menuname = menu;
   EFtonemap.menufunc = m_tonemap;
   EFtonemap.funcname = "tonemap";
   EFtonemap.Farea = 2;                                                          //  select area usable
   EFtonemap.Frestart = 1;                                                       //  restart allowed
   EFtonemap.FusePL = 1;                                                         //  use with paint/lever edits OK
   EFtonemap.Fscript = 1;                                                        //  scripting supported                15.10
   EFtonemap.threadfunc = Tmap_thread;
   if (! edit_setup(EFtonemap)) return;                                          //  setup: no preview, select area OK

/***
            _____________________________
           |                             |
           |                             |
           |    curve drawing area       |
           |                             |
           |_____________________________|
            low       contrast       high

            Amplify ========[]=========

            Curve File: [ Open ] [ Save ]
***/

   zdialog *zd = zdialog_new(title,Mwin,Bdone,Bcancel,null);
   EFtonemap.zd = zd;

   zdialog_add_widget(zd,"frame","frame","dialog",0,"expand");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labcL","hb1",ZTX("low"),"space=4");
   zdialog_add_widget(zd,"label","labcM","hb1",Bcontrast,"expand");
   zdialog_add_widget(zd,"label","labcH","hb1",ZTX("high"),"space=5");

   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labcon","hb2",ZTX("Amplify"),"space=5");
   zdialog_add_widget(zd,"hscale","amplify","hb2","0|1|0.005|0","expand");
   zdialog_add_widget(zd,"label","ampval","hb2",0,"space=5");

   zdialog_add_widget(zd,"hbox","hbcf","dialog",0,"space=8");
   zdialog_add_widget(zd,"label","labcf","hbcf",Bcurvefile,"space=5");
   zdialog_add_widget(zd,"button","loadcurve","hbcf",Bopen,"space=5");
   zdialog_add_widget(zd,"button","savecurve","hbcf",Bsave,"space=5");

   GtkWidget *frame = zdialog_widget(zd,"frame");                                //  set up curve edit
   spldat *sd = splcurve_init(frame,Tmap_curvedit);
   EFtonemap.curves = sd;

   sd->Nspc = 1;
   sd->fact[0] = 1;                                                              //  15.10
   sd->vert[0] = 0;
   sd->nap[0] = 3;                                                               //  initial curve anchor points
   sd->apx[0][0] = 0.01;
   sd->apy[0][0] = 0.10;
   sd->apx[0][1] = 0.40;
   sd->apy[0][1] = 0.30;
   sd->apx[0][2] = 0.99;
   sd->apy[0][2] = 0.01;

   splcurve_generate(sd,0);                                                      //  generate curve data

   Tmap_initz(zd);                                                               //  initialize
   return;
}


//  initialization for new image file

void tonemap_names::Tmap_initz(zdialog *zd)
{
   using namespace tonemap_names;

   int         ii, cc, px, py;
   float       b1, *pix1;
   int         jj, sum, limit, condist[100];

   ww = E1pxm->ww;                                                               //  image dimensions
   hh = E1pxm->hh;

   cc = ww * hh * sizeof(float);                                                 //  allocate brightness map memory
   brmap1 = (float *) zmalloc(cc);
   brmap2 = (float *) zmalloc(cc);
   for (ii = 0; ii < 4; ii++)
      brmap4[ii] = (float *) zmalloc(cc);

   for (py = 0; py < hh; py++)                                                   //  map initial image brightness
   for (px = 0; px < ww; px++)
   {
      ii = py * ww + px;
      pix1 = PXMpix(E1pxm,px,py);
      b1 = 0.333 * (pix1[0] + pix1[1] + pix1[2]);                                //  pixel brightness 0-255.9
      if (b1 < 1) b1 = 1;
      brmap1[ii] = b1;
   }

   for (ii = 0; ii < 100; ii++)
      condist[ii] = 0;

   for (py = 1; py < hh; py++)                                                   //  map contrast distribution
   for (px = 1; px < ww; px++)
   {
      ii = py * ww + px;
      jj = 0.388 * fabsf(brmap1[ii] - brmap1[ii-1]);                             //  horiz. contrast, ranged 0 - 99
      condist[jj]++;
      jj = 0.388 * fabsf(brmap1[ii] - brmap1[ii-ww]);                            //  vertical
      condist[jj]++;
   }

   sum = 0;
   limit = 0.99 * 2 * (ww-1) * (hh-1);                                           //  find 99th percentile contrast

   for (ii = 0; ii < 100; ii++) {
      sum += condist[ii];
      if (sum > limit) break;
   }

   contrast99 = 255.0 * ii / 100.0;                                              //  0 to 255
   if (contrast99 < 4) contrast99 = 4;                                           //  rescale low-contrast image

   zdialog_resize(zd,300,300);
   zdialog_run(zd,Tmap_dialog_event,"save");                                     //  run dialog - parallel

   amplify = 0;
   return;
}


//  dialog event and completion callback function

int tonemap_names::Tmap_dialog_event(zdialog *zd, cchar *event)
{
   using namespace tonemap_names;

   spldat   *sd = EFtonemap.curves;
   char     text[8];

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

   if (zd->zstat) {                                                              //  dialog complete
      if (zd->zstat == 1) edit_done(0);
      else edit_cancel(0);
      zfree(brmap1);                                                             //  free memory
      zfree(brmap2);
      brmap1 = brmap2 = 0;
      for (int ii = 0; ii < 4; ii++) {
         zfree(brmap4[ii]);
         brmap4[ii] = 0;
      }
      return 1;
   }
   
   if (strmatch(event,"apply"))                                                  //  from script                        15.10
      Tmap_dialog_event(zd,"amplify");

   if (strmatch(event,"blendwidth")) signal_thread();

   if (strmatch(event,"amplify")) {                                              //  slider value
      zdialog_fetch(zd,"amplify",amplify);
      snprintf(text,8,"%.2f",amplify);                                           //  numeric feedback
      zdialog_stuff(zd,"ampval",text);
      signal_thread();                                                           //  trigger update thread
   }

   if (strmatch(event,"loadcurve")) {                                            //  load saved curve
      splcurve_load(sd);
      signal_thread();
      return 0;
   }

   if (strmatch(event,"savecurve")) {                                            //  save curve to file
      splcurve_save(sd);
      return 0;
   }

   return 0;
}


//  this function is called when the curve is edited

void tonemap_names::Tmap_curvedit(int)
{
   signal_thread();
   return;
}


//  thread function

void * tonemap_names::Tmap_thread(void *)
{
   using namespace tonemap_names;

   int      ii;

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

      for (ii = 0; ii < 4; ii++)                                                 //  start working threads 1 (4)
         start_wthread(Tmap_wthread1,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

      for (ii = 0; ii < 4; ii++)                                                 //  start working threads 2 (4)
         start_wthread(Tmap_wthread2,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

      for (ii = 0; ii < NWT; ii++)                                               //  start working threads 3 (NWT)
         start_wthread(Tmap_wthread3,&Nval[ii]);
      wait_wthreads();                                                           //  wait for completion

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

      if (amplify == 0)
         CEF->Fmods = 0;

      Fpaint2();
   }

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


//  working threads

void * tonemap_names::Tmap_wthread1(void *arg)
{
   using namespace tonemap_names;

   int         ii, kk, bii, pii, dist = 0;
   int         px, px1, px2, pxinc;
   int         py, py1, py2, pyinc;
   float       b1, b4, xval, yval, grad;
   float       amp, con99;
   spldat      *sd = EFtonemap.curves;

   con99 = contrast99;                                                           //  99th percentile contrast
   con99 = 1.0 / con99;                                                          //  inverted

   amp = pow(amplify,0.5);                                                       //  get amplification, 0 to 1

   bii = *((int *) arg);                                                         //  thread 0...3

   if (bii == 0) {                                                               //  direction SE
      px1 = 1; px2 = ww; pxinc = 1;
      py1 = 1; py2 = hh; pyinc = 1;
      pii = - 1 - ww;
   }

   else if (bii == 1) {                                                          //  direction SW
      px1 = ww-2; px2 = 0; pxinc = -1;
      py1 = 1; py2 = hh; pyinc = 1;
      pii = + 1 - ww;
   }

   else if (bii == 2) {                                                          //  direction NE
      px1 = 1; px2 = ww; pxinc = 1;
      py1 = hh-2; py2 = 0; pyinc = -1;
      pii = - 1 + ww;
   }

   else {   /* bii == 3 */                                                       //  direction NW
      px1 = ww-2; px2 = 0; pxinc = -1;
      py1 = hh-2; py2 = 0; pyinc = -1;
      pii = + 1 + ww;
   }

   for (ii = 0; ii < ww * hh; ii++)                                              //  initial brightness map
      brmap4[bii][ii] = brmap1[ii];

   for (py = py1; py != py2; py += pyinc)                                        //  loop all image pixels
   for (px = px1; px != px2; px += pxinc)
   {
      ii = py * ww + px;

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

      b1 = brmap1[ii];                                                           //  this pixel brightness
      grad = b1 - brmap1[ii+pii];                                                //   - prior pixel --> gradient

      xval = fabsf(grad) * con99;                                                //  gradient scaled 0 to 1+
      kk = 1000.0 * xval;
      if (kk > 999) kk = 999;
      yval = 1.0 + 5.0 * sd->yval[0][kk];                                        //  amplifier = 1...6
      grad = grad * yval;                                                        //  magnified gradient

      b4 = brmap4[bii][ii+pii] + grad;                                           //  pixel brightness = prior + gradient
      b4 = (1.0 - amp) * b1 + amp * b4;                                          //  apply amplifier: b4 range = b1 --> b4

      brmap4[bii][ii] = b4;                                                      //  new pixel brightness
   }

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


void * tonemap_names::Tmap_wthread2(void *arg)
{
   using namespace tonemap_names;

   int      index, ii, px, py, dist = 0;
   float    b4;

   index = *((int *) arg);                                                       //  thread 0...3

   for (py = index; py < hh; py += 4)                                            //  loop all image pixels
   for (px = 0; px < ww; px++)
   {
      ii = py * ww + px;

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

      b4 = brmap4[0][ii] + brmap4[1][ii]                                         //  new brightness = average of four
         + brmap4[2][ii] + brmap4[3][ii];                                        //    calculated brightness surfaces
      b4 = 0.25 * b4;

      if (b4 < 1) b4 = 1;

      brmap2[ii] = b4;
   }

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


void * tonemap_names::Tmap_wthread3(void *arg)
{
   using namespace tonemap_names;

   float       *pix1, *pix3;
   int         index, ii, px, py, dist = 0;
   float       b1, b2, bf, f1, f2;
   float       red1, green1, blue1, red3, green3, blue3;

   index = *((int *) arg);

   for (py = index; py < hh; py += NWT)                                          //  loop all image pixels
   for (px = 0; px < ww; px++)
   {
      ii = py * ww + px;

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

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

      b1 = brmap1[ii];                                                           //  initial pixel brightness
      b2 = brmap2[ii];                                                           //  calculated new brightness

      bf = b2 / b1;                                                              //  brightness ratio

      red1 = pix1[0];                                                            //  input RGB
      green1 = pix1[1];
      blue1 = pix1[2];

      red3 = bf * red1;                                                          //  output RGB
      if (red3 > 255) red3 = 255;
      green3 = bf * green1;
      if (green3 > 255) green3 = 255;
      blue3 = bf * blue1;
      if (blue3 > 255) blue3 = 255;

      if (sa_stat == 3 && dist < sa_blend) {                                     //  select area is active,
///      f1 = 1.0 * dist / sa_blend;                                             //    blend changes over sa_blend
         f1 = sa_blendfunc(dist);                                                //  16.08
         f2 = 1.0 - f1;
         red3 = f1 * red3 + f2 * red1;
         green3 = f1 * green3 + f2 * green1;
         blue3 = f1 * blue3 + f2 * blue1;
      }

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

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


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

//  Resize (rescale) image
//
//  Output pixels are composites of input pixels, e.g. 2/3 size means
//  that 3x3 input pixels are mapped into 2x2 output pixels, and an
//  image size of 1000 x 600 becomes 667 x 400.

int       resize_ww0, resize_hh0;                                                //  original size
int       resize_ww1, resize_hh1;                                                //  new size
float     resize_lockratio;                                                      //  lock w/h ratio
editfunc  EFresize;
int    resize_dialog_event(zdialog *zd, cchar *event);
void * resize_thread(void *);


void m_resize(GtkWidget *, cchar *menu)
{
   F1_help_topic = "resize_image";
   
   char     rtext[12];

   EFresize.menuname = menu;
   EFresize.menufunc = m_resize;
   EFresize.funcname = "resize";
   EFresize.Frestart = 1;                                                        //  allow restart
   EFresize.Fscript = 1;                                                         //  scripting supported                15.10
   EFresize.threadfunc = resize_thread;                                          //  thread function
   if (! edit_setup(EFresize)) return;                                           //  setup edit

/****
          _________________________________________________________________
         |                                                                 |
         |             pixels       percent                                |
         |  width     [_______]    [_______]                               |
         |  height    [_______]    [_______]                               |
         |                                                                 |
         |  [x] 1/1  [_] 3/4  [_] 2/3  [_] 1/2  [_] 1/3  [_] 1/4  [_] Prev |
         |  W/H Ratio: 1.333    Lock [_]                                   |
         |                                                [done] [cancel]  |
         |_________________________________________________________________|
       
****/

   zdialog *zd = zdialog_new(ZTX("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,"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,"spin","wpix","vb12","20|20000|1|20");
   zdialog_add_widget(zd,"spin","hpix","vb12","20|20000|1|20");
   zdialog_add_widget(zd,"label","labpct","vb13",Bpercent);
   zdialog_add_widget(zd,"spin","wpct","vb13","1|500|0.1|100");
   zdialog_add_widget(zd,"spin","hpct","vb13","1|500|0.1|100");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","space","hb2",0);
   zdialog_add_widget(zd,"check","1/1","hb2","1/1");
   zdialog_add_widget(zd,"check","3/4","hb2","3/4");
   zdialog_add_widget(zd,"check","2/3","hb2","2/3");
   zdialog_add_widget(zd,"check","1/2","hb2","1/2");
   zdialog_add_widget(zd,"check","1/3","hb2","1/3");
   zdialog_add_widget(zd,"check","1/4","hb2","1/4");
   zdialog_add_widget(zd,"check","prev","hb2",Bprev);
   zdialog_add_widget(zd,"hbox","hb3","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labratio","hb3",ZTX("W/H Ratio:"),"space=5");
   zdialog_add_widget(zd,"label","ratio","hb3","1.333");
   zdialog_add_widget(zd,"check","lockratio","hb3",ZTX("Lock"),"space=10");
   
   zdialog_add_ttip(zd,"prev",ZTX("use previous settings"));

   resize_ww0 = E1pxm->ww;                                                       //  original width, height
   resize_hh0 = E1pxm->hh;
   zdialog_stuff(zd,"wpix",resize_ww0);
   zdialog_stuff(zd,"hpix",resize_hh0);
   zdialog_stuff(zd,"lockratio",1);                                              //  default ratio locked
   
   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_stuff(zd,"prev",0);

   resize_lockratio = 1.0 * resize_ww0 / resize_hh0;                             //  15.10
   snprintf(rtext,12,"%.3f",resize_lockratio);
   zdialog_stuff(zd,"ratio",rtext);

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


//  dialog event and completion callback function

int resize_dialog_event(zdialog *zd, cchar *event)                               //  overhauled for scripting           15.10
{
   int         lock, nn;
   float       wpct1, hpct1, ratio;
   char        rtext[12];

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

   if (zd->zstat)                                                                //  dialog complete
   {
      if (zd->zstat == 1) {                                                      //  done
         editresize[0] = resize_ww1;                                             //  remember size used
         editresize[1] = resize_hh1;
         edit_done(0);
      }
      else edit_cancel(0);                                                       //  cancel or kill
      Fzoom = 0;
      return 1;
   }

   if (strmatch(event,"focus")) return 1;                                        //  ignore focus
   
   if (event[0] =='w' || event[0] == 'h') {                                      //  if width or height set directly,
      zdialog_stuff(zd,"1/1",0);                                                 //    set all ratio buttons off
      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,"prev",0);
   }
   
   if (strstr("1/1 3/4 2/3 1/2 1/3 1/4 prev",event)) {                           //  a ratio button was selected
      zdialog_stuff(zd,"1/1",0);                                                 //  set all ratio buttons off
      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,"prev",0);
      zdialog_stuff(zd,event,1);                                                 //  set selected button on
   }

   zdialog_fetch(zd,"wpix",resize_ww1);                                          //  get width and height values
   zdialog_fetch(zd,"hpix",resize_hh1);
   zdialog_fetch(zd,"wpct",wpct1);
   zdialog_fetch(zd,"hpct",hpct1);
   zdialog_fetch(zd,"lockratio",lock);                                           //  lock ratio on/off
   
   if (strmatch(event,"lockratio") && lock)                                      //  lock ratio enabled
      resize_lockratio = 1.0 * resize_ww1 / resize_hh1;                          //  update lock ratio to hold   15.10
   
   if (strmatch(event,"wpct"))                                                   //  width % - set pixel width
      resize_ww1 = int(wpct1 / 100.0 * resize_ww0 + 0.5);

   if (strmatch(event,"hpct"))                                                   //  height % - set pixel height
      resize_hh1 = int(hpct1 / 100.0 * resize_hh0 + 0.5);

   if (lock && event[0] == 'w')                                                  //  preserve width/height ratio
      resize_hh1 = int(resize_ww1 * 1.0 / resize_lockratio + 0.5);

   if (lock && event[0] == 'h')
      resize_ww1 = int(resize_hh1 * resize_lockratio + 0.5);

   zdialog_fetch(zd,"1/1",nn);                                                   //  if a ratio was selected,
   if (nn) {                                                                     //    set image size accordingly
      resize_ww1 = resize_ww0;
      resize_hh1 = resize_hh0;
   }

   zdialog_fetch(zd,"3/4",nn);
   if (nn) {
      resize_ww1 = (3 * resize_ww0 + 3) / 4;
      resize_hh1 = (3 * resize_hh0 + 3) / 4;
   }

   zdialog_fetch(zd,"2/3",nn);
   if (nn) {
      resize_ww1 = (2 * resize_ww0 + 2) / 3;
      resize_hh1 = (2 * resize_hh0 + 2) / 3;
   }

   zdialog_fetch(zd,"1/2",nn);
   if (nn) {
      resize_ww1 = (resize_ww0 + 1) / 2;
      resize_hh1 = (resize_hh0 + 1) / 2;
   }

   zdialog_fetch(zd,"1/3",nn);
   if (nn) {
      resize_ww1 = (resize_ww0 + 2) / 3;
      resize_hh1 = (resize_hh0 + 2) / 3;
   }

   zdialog_fetch(zd,"1/4",nn);
   if (nn) {
      resize_ww1 = (resize_ww0 + 3) / 4;
      resize_hh1 = (resize_hh0 + 3) / 4;
   }

   zdialog_fetch(zd,"prev",nn);                                                  //  set previously used size
   if (nn) {
      resize_ww1 = editresize[0];
      resize_hh1 = editresize[1];
   }

   if (resize_ww1 < 20 || resize_hh1 < 20) return 1;                             //  refuse tiny size

   hpct1 = 100.0 * resize_hh1 / resize_hh0;                                      //  set percents to match pixels
   wpct1 = 100.0 * resize_ww1 / resize_ww0;

   zdialog_stuff(zd,"wpix",resize_ww1);                                          //  update all widget values
   zdialog_stuff(zd,"hpix",resize_hh1);
   zdialog_stuff(zd,"wpct",wpct1);
   zdialog_stuff(zd,"hpct",hpct1);
   
   ratio = 1.0 * resize_ww1 / resize_hh1;                                        //  update w/h ratio                   15.10
   snprintf(rtext,12,"%.3f",ratio);
   zdialog_stuff(zd,"ratio",rtext);
   //resize_lockratio = ratio;                                                   //  update lock ratio  removed         16.02.1

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


//  do the resize job

void * resize_thread(void *)
{
   PXM   *pxmtemp1, *pxmtemp2;

   while (true)
   {
      thread_idle_loop();                                                        //  wait for signal
      pxmtemp1 = PXM_rescale(E1pxm,resize_ww1,resize_hh1);                       //  rescale the edit image
      paintlock(1);                                                              //  block window updates               16.02
      pxmtemp2 = E3pxm;
      E3pxm = pxmtemp1;
      PXM_free(pxmtemp2);
      paintlock(0);                                                              //  unblock window updates             16.02
      CEF->Fmods++;
      CEF->Fsaved = 0;
      Fpaint2();
   }

   return 0;
}


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

//  flip an image horizontally or vertically

editfunc    EFflip;

void m_flip(GtkWidget *, cchar *)
{
   int flip_dialog_event(zdialog *zd, cchar *event);

   F1_help_topic = "flip_image";

   EFflip.menufunc = m_flip;
   EFflip.funcname = "flip";
   EFflip.Frestart = 1;                                                          //  allow restart
   if (! edit_setup(EFflip)) return;                                             //  setup edit

   zdialog *zd = zdialog_new(ZTX("Flip"),Mwin,Bdone,Bcancel,null);
   EFflip.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","horz","hb1",ZTX("horizontal"),"space=5");
   zdialog_add_widget(zd,"button","vert","hb1",ZTX("vertical"),"space=5");

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

   return;
}


//  dialog event and completion callback function

int flip_dialog_event(zdialog *zd, cchar *event)
{
   int flip_horz();
   int flip_vert();

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

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

   if (strmatch(event,"horz")) flip_horz();
   if (strmatch(event,"vert")) flip_vert();
   return 1;
}


int flip_horz()
{
   int      px, py;
   float    *pix3, *pix9;
   int      nc = E3pxm->nc, pcc = nc * sizeof(float);

   E9pxm = PXM_copy(E3pxm);

   for (py = 0; py < E3pxm->hh; py++)
   for (px = 0; px < E3pxm->ww; px++)
   {
      pix3 = PXMpix(E3pxm,px,py);                                                //  image9 = flipped image3
      pix9 = PXMpix(E9pxm,E3pxm->ww-1-px,py);
      memcpy(pix9,pix3,pcc);
   }

   PXM_free(E3pxm);                                                              //  image9 >> image3
   E3pxm = E9pxm;
   E9pxm = 0;

   CEF->Fmods++;
   CEF->Fsaved = 0;
   Fpaint2();
   return 0;
}


int flip_vert()
{
   int      px, py;
   float    *pix3, *pix9;
   int      nc = E3pxm->nc, pcc = nc * sizeof(float);

   E9pxm = PXM_copy(E3pxm);

   for (py = 0; py < E3pxm->hh; py++)
   for (px = 0; px < E3pxm->ww; px++)
   {
      pix3 = PXMpix(E3pxm,px,py);                                                //  image9 = flipped image3
      pix9 = PXMpix(E9pxm,px,E3pxm->hh-1-py);
      memcpy(pix9,pix3,pcc);
   }

   PXM_free(E3pxm);                                                              //  image9 >> image3
   E3pxm = E9pxm;
   E9pxm = 0;

   CEF->Fmods++;
   CEF->Fsaved = 0;
   Fpaint2();
   return 0;
}


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

//  write text on top of the image

namespace writetext
{
   #define Bversion ZTX("+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    EFwritetext;
}


//  menu function

void m_write_text(GtkWidget *, cchar *menu)
{
   using namespace writetext;

   cchar    *title = ZTX("Write Text on Image");
   cchar    *tip = ZTX("Enter text, click/drag on image, right click to remove");

   F1_help_topic = "add_text";                                                   //  user guide topic

   EFwritetext.menufunc = m_write_text;
   EFwritetext.funcname = "write_text";
   EFwritetext.Farea = 1;                                                        //  select area ignored
   EFwritetext.Frestart = 1;                                                     //  allow restart
   if (! edit_setup(EFwritetext)) return;                                        //  setup edit

/***
       ____________________________________________________________________
      |                     Write Text on Image                            |
      |                                                                    |
      |  Enter text, click/drag on image, right click to remove.           |
      |                                                                    |
      |  Text [____________________________________________________]       |     text
      |  Use metadata key [________________________________] [Fetch]       |     metakey Bfetch
      |  Use text file  [Open] [Save]                                      |     Bopen Bsave
      |                                                                    |
      |  [Font] [FreeSans_________]  Size [ 44|v]                          |     Bfont fontname fontsize
      |                                                                    |
      |            color  transparency   width        angle                |
      |  text     [#####] [_______|-+]              [______|-+]            |     fgcolor fgtransp fgangle
      |  backing  [#####] [_______|-+]                                     |     bgcolor bgtransp
      |  outline  [#####] [_______|-+] [_______|-+]                        |     tocolor totransp towidth
      |  shadow   [#####] [_______|-+] [_______|-+] [______|-+]            |     shcolor shtransp shwidth shangle
      |                                                                    |
      |        [Clear] [Replace] [+Version] [Next] [Apply] [Done] [Cancel] |     Bclear Breplace Bversion Bnext 
      |____________________________________________________________________|       Bapply Bdone Bcancel

      [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);
   EFwritetext.zd = zd;
   EFwritetext.mousefunc = mousefunc;
   EFwritetext.menufunc = m_write_text;                                          //  allow restart

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

   zdialog_add_widget(zd,"hbox","hbtext","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labtext","hbtext",ZTX("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");
   zdialog_add_widget(zd,"label","labmeta","hbmeta",ZTX("Use metadata key"),"space=5");
   zdialog_add_widget(zd,"entry","metakey","hbmeta",0,"space=2|expand");
   zdialog_add_widget(zd,"button",Bfetch,"hbmeta",Bfetch);

   zdialog_add_widget(zd,"hbox","hbfile","dialog");
   zdialog_add_widget(zd,"label","labfile","hbfile",ZTX("Use text file"),"space=3");
   zdialog_add_widget(zd,"button",Bopen,"hbfile",Bopen);
   zdialog_add_widget(zd,"button",Bsave,"hbfile",Bsave);
   zdialog_add_widget(zd,"button",Bclear,"hbfile",Bclear,"space=30");

   zdialog_add_widget(zd,"hbox","hbfont","dialog");
   zdialog_add_widget(zd,"button",Bfont,"hbfont",Bfont);
   zdialog_add_widget(zd,"entry","fontname","hbfont","FreeSans","space=2|expand");
   zdialog_add_widget(zd,"label","labfsize","hbfont",Bsize,"space=2");
   zdialog_add_widget(zd,"spin","fontsize","hbfont","8|500|1|40");

   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",ZTX("text"));
   zdialog_add_widget(zd,"label","labback","vbattr1",ZTX("backing"));
   zdialog_add_widget(zd,"label","laboutln","vbattr1",ZTX("outline"));
   zdialog_add_widget(zd,"label","labshadow","vbattr1",ZTX("shadow"));

   zdialog_add_widget(zd,"label","labcol","vbattr2",Bcolor);
   zdialog_add_widget(zd,"colorbutt","fgcolor","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",Btransparency);
   zdialog_add_widget(zd,"spin","fgtransp","vbattr3","0|100|1|0");
   zdialog_add_widget(zd,"spin","bgtransp","vbattr3","0|100|1|0");
   zdialog_add_widget(zd,"spin","totransp","vbattr3","0|100|1|0");
   zdialog_add_widget(zd,"spin","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,"spin","towidth","vbattr4","0|30|1|0");
   zdialog_add_widget(zd,"spin","shwidth","vbattr4","0|50|1|0");

   zdialog_add_widget(zd,"label","labw","vbattr5",Bangle);
   zdialog_add_widget(zd,"spin","fgangle","vbattr5","-180|180|0.5|0");
   zdialog_add_widget(zd,"label","space","vbattr5");
   zdialog_add_widget(zd,"label","space","vbattr5");
   zdialog_add_widget(zd,"spin","shangle","vbattr5","-180|180|1|0");

   zdialog_add_ttip(zd,Breplace,ZTX("save to current file"));
   zdialog_add_ttip(zd,Bversion,ZTX("save as new file version"));
   zdialog_add_ttip(zd,Bnext,ZTX("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,"fgcolor",attr.color[0],20);
   zdialog_fetch(zd,"fgtransp",attr.transp[0]);
   zdialog_fetch(zd,"fgangle",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 writetext::dialog_event(zdialog *zd, cchar *event)
{
   using namespace writetext;

   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 (strmatch(event,"escape")) zd->zstat = 7;                                  //  escape = cancel                    15.07

   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
      }
   
      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);                         //  replace curr. file
         curr_file_size = f_save_size;
         m_write_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);                 //  save file
         if (! err) f_open_saved();                                              //  open saved file with edit hist
         zfree(newfilename);
         m_write_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);                         //  replace curr. file
         curr_file_size = f_save_size;
         m_next(0,0);                                                            //  open next file
         m_write_text(0,0);                                                      //  start again
         write(1);                                                               //  put same text etc. onto image
         return 1;
      }
      
      if (zd->zstat == 5) {                                                      //  apply
         if (! textpresent) return 1;
         edit_done(0);                                                           //  save mods
         m_write_text(0,0);                                                      //  restart
         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 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,"fgcolor",attr.color[0],20);
      zdialog_fetch(zd,"fgtransp",attr.transp[0]);
      zdialog_fetch(zd,"fgangle",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(ZTX("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,"fgangle"))
      zdialog_fetch(zd,"fgangle",attr.angle);

   if (strmatch(event,"fgcolor"))                                                //  foreground (text) color
      zdialog_fetch(zd,"fgcolor",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,"fgtransp"))                                               //  foreground transparency
      zdialog_fetch(zd,"fgtransp",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

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


//  mouse function, set position for text on image

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

   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 writetext::write(int mode)
{
   using namespace writetext;

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

   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);                                           //  update window to erase old text
         textpresent = 0;                                                        //  mark no text present
         CEF->Fmods--;
      }
      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                       16.02
         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);                                              //  update window to erase old text
      textpresent = 0;
      CEF->Fmods--;
   }
                                                                                 //  (updates together to reduce flicker)
   Fpaint3(orgx2,orgy2,ww2,hh2);                                                 //  update window for new text

   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",writetext_dirk);                      //  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 = strstr(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,"fgangle",attr.angle);
   zdialog_stuff(zd,"fgcolor",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,"fgtransp",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,ZTX("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",writetext_dirk);                      //  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,"fgangle",attr.angle);
   zdialog_fetch(zd,"fgcolor",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,"fgtransp",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            fgred, fggreen, fgblue;
   int            bgred, bggreen, bgblue;
   int            tored, togreen, toblue;
   int            shred, shgreen, shblue;
   float          fgtransp, 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          fgpart, topart, shpart, bgpart;

   zthreadcrash();                                                               //  thread usage not allowed

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

   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);
   fgred = atoi(strField(attr->color[0],'|',1));                                 //  get text foreground color
   fggreen = atoi(strField(attr->color[0],'|',2));
   fgblue = 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));
   fgtransp = 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(Cdrawin,null);
   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,hh,0);                                                //  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,py);
      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,1);                                                //  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

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

      red = fgpart * fgred + topart * tored + shpart * shred + bgpart * bgred;
      green = fgpart * fggreen + topart * togreen + shpart * shgreen + bgpart * bggreen;
      blue = fgpart * fgblue + 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 - fgpart - 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,0);                                                   //  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,0);                                                   //  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


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

//  write a line or arrow on top of the image

namespace writeline
{
   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    EFwriteline;
}


void m_write_line(GtkWidget *, cchar *menu)
{
   using namespace writeline;

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

   F1_help_topic = "add_lines";                                                  //  user guide topic

   EFwriteline.menufunc = m_write_line;
   EFwriteline.funcname = "write_line";
   EFwriteline.Farea = 1;                                                        //  select area ignored
   if (! edit_setup(EFwriteline)) return;                                        //  setup edit

/***
       ____________________________________________________________
      |               Write 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  transparency   width        angle        |
      |  line     [#####] [_______|-+]              [______|-+]    |       fgcolor fgtransp fgangle
      |  backing  [#####] [_______|-+]                             |       bgcolor bgtransp
      |  outline  [#####] [_______|-+] [_______|-+]                |       tocolor totransp towidth
      |  shadow   [#####] [_______|-+] [_______|-+] [______|-+]    |       shcolor shtransp shwidth shangle
      |                                                            |
      |                                    [Apply] [Done] [Cancel] |
      |____________________________________________________________|

***/

   zd = zdialog_new(ZTX("Write Line or Arrow on Image"),Mwin,Bapply,Bdone,Bcancel,null);
   EFwriteline.zd = zd;
   EFwriteline.mousefunc = mousefunc;
   EFwriteline.menufunc = m_write_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",ZTX("Line length"),"space=5");
   zdialog_add_widget(zd,"spin","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,"spin","width","hbline","1|99|1|2");

   zdialog_add_widget(zd,"hbox","hbarrow","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labarrow","hbarrow",ZTX("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",ZTX("line"));
   zdialog_add_widget(zd,"label","labback","vbcol1",ZTX("backing"));
   zdialog_add_widget(zd,"label","laboutln","vbcol1",ZTX("outline"));
   zdialog_add_widget(zd,"label","labshadow","vbcol1",ZTX("shadow"));

   zdialog_add_widget(zd,"label","labcol","vbcol2",Bcolor);
   zdialog_add_widget(zd,"colorbutt","fgcolor","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",Btransparency);
   zdialog_add_widget(zd,"spin","fgtransp","vbcol3","0|100|1|0");
   zdialog_add_widget(zd,"spin","bgtransp","vbcol3","0|100|1|0");
   zdialog_add_widget(zd,"spin","totransp","vbcol3","0|100|1|0");
   zdialog_add_widget(zd,"spin","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,"spin","towidth","vbcol4","0|30|1|0");
   zdialog_add_widget(zd,"spin","shwidth","vbcol4","0|50|1|0");

   zdialog_add_widget(zd,"label","labw","vbcol5",Bangle);
   zdialog_add_widget(zd,"spin","fgangle","vbcol5","-180|180|0.1|0");
   zdialog_add_widget(zd,"label","space","vbcol5");
   zdialog_add_widget(zd,"label","space","vbcol5");
   zdialog_add_widget(zd,"spin","shangle","vbcol5","-180|180|1|0");

   zdialog_add_ttip(zd,Bapply,ZTX("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,"fgangle",attr.angle);
   zdialog_fetch(zd,"fgcolor",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,"fgtransp",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 writeline::dialog_event(zdialog *zd, cchar *event)
{
   using namespace writeline;

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

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  Apply, commit present line to image
         write(1);
         linepresent = 0;                                                        //  (no old line to erase)
         mpx = mpy = -1;
         zd->zstat = 0;
         edit_done(0);                                                           //  finish
         m_write_line(0,0);                                                      //  start again
         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,"fgangle"))                                                //  line angle
      zdialog_fetch(zd,"fgangle",attr.angle);

   if (strmatch(event,"fgcolor"))                                                //  foreground (line) color
      zdialog_fetch(zd,"fgcolor",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,"fgtransp"))                                               //  foreground transparency
      zdialog_fetch(zd,"fgtransp",attr.transp[0]);

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

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

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

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

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

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

   genline(&attr);                                                               //  build line image from attributes
   write(1);                                                                     //  write on image
   return 1;
}


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

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

   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;                                                     //  bugfix 16.04
      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;                                                     //  bugfix 16.04
      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,"fgangle",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 writeline::write(int mode)
{
   using namespace writeline;

   float       *pix1, *pix3;
   uint8       *pixL;
   int         px1, py1, px3, py3, done;
   float       e3part, Ot, Om, Ob;
   int         nc = E1pxm->nc, pcc = nc * sizeof(float);

   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);                                           //  update window to erase old line
         linepresent = 0;                                                        //  mark no line present
      }
      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                       16.02
         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);                                              //  update window to erase old line
      linepresent = 0;
   }
                                                                                 //  (updates together to reduce flicker)
   Fpaint3(orgx2,orgy2,ww2,hh2);                                                 //  update window for new line

   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            fgred, fggreen, fgblue;
   int            bgred, bggreen, bgblue;
   int            tored, togreen, toblue;
   int            shred, shgreen, shblue;
   float          fgtransp, 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          fgpart, topart, shpart, bgpart;

   zthreadcrash();                                                               //  thread usage not allowed

   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);
   fgred = atoi(strField(attr->color[0],'|',1));                                 //  get line foreground color
   fggreen = atoi(strField(attr->color[0],'|',2));
   fgblue = 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));
   fgtransp = 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,0);                                                //  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,1);                                                //  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

      fgpart = pix1[0] / 256.0;
      topart = pix1[1] / 256.0;
      shpart = pix1[2] / 256.0;
      bgpart = (1.0 - fgpart - topart - shpart);
      fgpart = fgpart * (1.0 - fgtransp);
      topart = topart * (1.0 - totransp);
      shpart = shpart * (1.0 - shtransp);
      bgpart = bgpart * (1.0 - bgtransp);

      red = fgpart * fgred + topart * tored + shpart * shred + bgpart * bgred;
      green = fgpart * fggreen + topart * togreen + shpart * shgreen + bgpart * bggreen;
      blue = fgpart * fgblue + 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 - fgpart - 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,0);                                                   //  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,0);                                                   //  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


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

//  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 = ZTX("Paint Edits");
   cchar    *helptext = ZTX("Press F1 for help");
   int      yn, cc;

   F1_help_topic = "paint_edits";

   if (sa_stat || zdsela) {                                                      //  warn select area will be lost      15.08
      yn = zmessageYN(Mwin,ZTX("Select area cannot be kept.\n"
                               "Continue?"));
      if (! yn) return;
      sa_unselect();                                                             //  unselect area
      if (zdsela) zdialog_free(zdsela);
   }

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

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

***/

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

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

   cc = Fpxb->ww * Fpxb->hh * sizeof(uint16);                                    //  allocate sa_pixmap[] for area
   sa_pixmap = (uint16 *) zmalloc(cc);
   memset(sa_pixmap,0,cc);                                                       //  edge distance = 0 for all pixels

   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_blend = 10000;                                                             //  "blend width"
   sa_fww = Fpxb->ww;                                                            //  valid image dimensions
   sa_fhh = Fpxb->hh;
   areanumber++;                                                                 //  next sequential number

   zdialog_run(zdsela,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_blend = maximum edit power (edit fully applied)

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

   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (zd->zstat)                                                                //  done or cancel
   {
      freeMouse();                                                               //  disconnect mouse function
      if (CEF) zdialog_send_event(CEF->zd,"done");                               //  complete edit                      15.08
      zdialog_free(zdsela);                                                      //  kill dialog
      sa_unselect();                                                             //  unselect area
      return 0;
   }

   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_unselect();                                                             //  unselect current area if any
      cc = Fpxb->ww * Fpxb->hh * sizeof(uint16);                                 //  allocate sa_pixmap[] for new area
      sa_pixmap = (uint16 *) zmalloc(cc);
      memset(sa_pixmap,0,cc);                                                    //  edge distance = 0 for all pixels

      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_blend = 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()
{
   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);                                     //  show mouse selection circle

   if (LMclick || RMclick)                                                       //  mouse click, process normally
      return;

   if (Mbutton != 1 && Mbutton != 3)                                             //  button released
      return;

   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 > 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
      {                                                                          //  increase edit power
         sa_pixmap[ii] += 2.0 * power;
         if (sa_pixmap[ii] > sa_blend) sa_pixmap[ii] = sa_blend;
      }

      if (Mbutton == 3)                                                          //  right mouse button
      {                                                                          //  weaken edit power
         if (sa_pixmap[ii] <= power) sa_pixmap[ii] = 0;                          //  erase is half as fast as paint
         else sa_pixmap[ii] -= power;
      }
   }

   zdialog_send_event(CEF->zd,"blendwidth");                                     //  notify edit dialog

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


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

//  Use the image brightness or color values to leverage subsequent edits.
//  Method:
//  Select the whole image as an area.
//  Set "edge distance" 1 to 999 from pixel brightness or RGB color.
//  Set "blend width" to 999.
//  Edit function coefficient = edge distance / blend width.

spldat   *leveds_curve;
int      leveds_type, leveds_color;
int      leveds_ptype, leveds_pcolor;
float    *leveds_lever = 0;


//  menu function

void m_lever_edits(GtkWidget *, cchar *)
{
   int    leveds_event(zdialog *, cchar *event);                                 //  dialog event and completion func
   void   leveds_curve_update(int spc);                                          //  curve update callback function

   cchar    *title = ZTX("Leverage Edits");
   cchar    *legend = ZTX("Edit Function Amplifier");
   int      yn;

   F1_help_topic = "leverage_edits";

   if (sa_stat || zdsela) {                                                      //  warn select area will be lost      15.08
      yn = zmessageYN(Mwin,ZTX("Select area cannot be kept.\n"
                               "Continue?"));
      if (! yn) return;
      sa_unselect();                                                             //  unselect area
      if (zdsela) zdialog_free(zdsela);
   }

   if (! CEF) {                                                                  //  edit func must be active
      zmessageACK(Mwin,ZTX("Edit function must be active"));
      return;
   }

   if (! CEF->FusePL) {
      zmessageACK(Mwin,ZTX("Cannot use Leverage Edits"));
      return;
   }

   if (CEF->Fpreview) 
      zdialog_send_event(CEF->zd,"fullsize");                                    //  use full-size image                16.04

/***
                  Edit Function Amplifier
             ------------------------------------------
            |                                          |
            |                                          |
            |           curve drawing area             |
            |                                          |
            |                                          |
             ------------------------------------------
             minimum                            maximum

             [+++]  [---]  [+ -]  [- +]  [+-+]  [-+-]
             (o) Brightness  (o) Contrast
             (o) All  (o) Red  (o) Green  (o) Blue
             Curve File: [ Open ] [ Save ]
                                              [ Done ]
***/

   zdsela = zdialog_new(title,Mwin,Bdone,null);

   zdialog_add_widget(zdsela,"label","labt","dialog",legend);
   zdialog_add_widget(zdsela,"frame","fr1","dialog",0,"expand");
   zdialog_add_widget(zdsela,"hbox","hba","dialog");
   zdialog_add_widget(zdsela,"label","labda","hba",ZTX("minimum"),"space=5");
   zdialog_add_widget(zdsela,"label","space","hba",0,"expand");
   zdialog_add_widget(zdsela,"label","labba","hba",ZTX("maximum"),"space=5");
   zdialog_add_widget(zdsela,"hbox","hbb","dialog",0,"space=10");
   zdialog_add_widget(zdsela,"button","b +++","hbb","+++","space=3");
   zdialog_add_widget(zdsela,"button","b ---","hbb","‒ ‒ ‒","space=3");
   zdialog_add_widget(zdsela,"button","b +-", "hbb"," + ‒ ","space=3");
   zdialog_add_widget(zdsela,"button","b -+", "hbb"," ‒ + ","space=3");
   zdialog_add_widget(zdsela,"button","b +-+","hbb","+ ‒ +","space=3");
   zdialog_add_widget(zdsela,"button","b -+-","hbb","‒ + ‒","space=3");

   zdialog_add_widget(zdsela,"hbox","hbbr1","dialog");
   zdialog_add_widget(zdsela,"radio","bright","hbbr1",Bbrightness,"space=5");
   zdialog_add_widget(zdsela,"radio","contrast","hbbr1",Bcontrast,"space=5");
   zdialog_add_widget(zdsela,"hbox","hbbr2","dialog");
   zdialog_add_widget(zdsela,"radio","all","hbbr2",Ball,"space=5");
   zdialog_add_widget(zdsela,"radio","red","hbbr2",Bred,"space=5");
   zdialog_add_widget(zdsela,"radio","green","hbbr2",Bgreen,"space=5");
   zdialog_add_widget(zdsela,"radio","blue","hbbr2",Bblue,"space=5");

   zdialog_add_widget(zdsela,"hbox","hbcf","dialog",0,"space=5");
   zdialog_add_widget(zdsela,"label","labcf","hbcf",Bcurvefile,"space=5");
   zdialog_add_widget(zdsela,"button","loadcurve","hbcf",Bopen,"space=5");
   zdialog_add_widget(zdsela,"button","savecurve","hbcf",Bsave,"space=5");

   GtkWidget *frame = zdialog_widget(zdsela,"fr1");                              //  setup for curve editing
   spldat *sd = splcurve_init(frame,leveds_curve_update);
   leveds_curve = sd;

   sd->Nspc = 1;
   sd->vert[0] = 0;
   sd->nap[0] = 3;                                                               //  initial curve anchor points
   sd->fact[0] = 1;                                                              //  15.10
   sd->apx[0][0] = 0.01;
   sd->apy[0][0] = 0.5;
   sd->apx[0][1] = 0.50;
   sd->apy[0][1] = 0.5;
   sd->apx[0][2] = 0.99;
   sd->apy[0][2] = 0.5;
   splcurve_generate(sd,0);                                                      //  generate curve data

   zdialog_stuff(zdsela,"bright",1);                                             //  type leverage = brightness
   zdialog_stuff(zdsela,"contrast",0);
   zdialog_stuff(zdsela,"all",1);                                                //  color used = all
   zdialog_stuff(zdsela,"red",0);
   zdialog_stuff(zdsela,"green",0);
   zdialog_stuff(zdsela,"blue",0);
   leveds_type = 1;
   leveds_color = 1;
   leveds_ptype = 0;
   leveds_pcolor = 0;

   int cc = Fpxb->ww * Fpxb->hh * sizeof(uint16);                                //  allocate sa_pixmap[] for area
   sa_pixmap = (uint16 *) zmalloc(cc);
   memset(sa_pixmap,0,cc);                                                       //  edge distance = 0 for all pixels

   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_blend = 999;                                                               //  "blend width" = 999
   sa_fww = Fpxb->ww;                                                            //  valid image dimensions
   sa_fhh = Fpxb->hh;
   areanumber++;                                                                 //  next sequential number

   if (leveds_lever) zfree(leveds_lever);
   cc = E1pxm->ww * E1pxm->hh * sizeof(float);                                   //  allocate memory for lever
   leveds_lever = (float *) zmalloc(cc);

   zdialog_resize(zdsela,0,360);
   zdialog_run(zdsela,leveds_event,"save");                                      //  run dialog - parallel
   leveds_event(zdsela,"init");                                                  //  initialize default params
   return;
}


//  dialog event and completion function

int leveds_event(zdialog *zd, cchar *event)
{
   int         ii, kk, pixdist;
   float       px, py, xval, yval, lever;
   float       *pixel0, *pixel1, *pixel2, *pixel3, *pixel4;
   spldat      *sd = leveds_curve;
   
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (zd->zstat) {                                                              //  done or cancel
      if (CEF && CEF->zd) zdialog_send_event(CEF->zd,"done");                    //  notify edit dialog                 16.04
      sa_unselect();                                                             //  delete area
      zdialog_free(zdsela);
      zfree(sd);                                                                 //  free curve edit memory
      zfree(leveds_lever);
      leveds_lever = 0;
      return 1;
   }

   if (! sa_validate() || sa_stat != 3) {                                        //  select area gone
      zdialog_free(zdsela);
      zfree(sd);                                                                 //  free curve edit memory
      zfree(leveds_lever);
      leveds_lever = 0;
      return 1;
   }

   if (strmatch(event,"loadcurve")) {                                            //  load saved curve
      splcurve_load(sd);
      if (CEF && CEF->zd) zdialog_send_event(CEF->zd,"blendwidth");              //  notify edit dialog
      return 1;
   }

   if (strmatch(event,"savecurve")) {                                            //  save curve to file
      splcurve_save(sd);
      return 1;
   }

   ii = strmatchV(event,"bright","contrast",null);                               //  new lever type
   if (ii >= 1 && ii <= 2) leveds_type = ii;

   ii = strmatchV(event,"all","red","green","blue",null);                        //  new lever color
   if (ii >= 1 && ii <= 4) leveds_color = ii;

   if (leveds_type != leveds_ptype || leveds_color != leveds_pcolor)             //  test for change
   {
      if (! CEF) return 1;                                                       //  edit canceled                      15.08

      leveds_ptype = leveds_type;
      leveds_pcolor = leveds_color;

      for (int ipy = 1; ipy < E1pxm->hh-1; ipy++)
      for (int ipx = 1; ipx < E1pxm->ww-1; ipx++)
      {
         pixel0 = PXMpix(E1pxm,ipx,ipy);                                         //  target pixel to measure
         lever = 0;

         if (leveds_type == 1)                                                   //  lever type = brightness
         {
            if (leveds_color == 1)
               lever = 0.333 * (pixel0[0] + pixel0[1] + pixel0[2]);              //  use all colors
            else {
               ii = leveds_color - 2;                                            //  use single color
               lever = pixel0[ii];
            }
         }

         else if (leveds_type == 2)                                              //  lever type = contrast
         {
            pixel1 = PXMpix(E1pxm,ipx-1,ipy);                                    //  15.09
            pixel2 = PXMpix(E1pxm,ipx+1,ipy);
            pixel3 = PXMpix(E1pxm,ipx,ipy-1);
            pixel4 = PXMpix(E1pxm,ipx,ipy+1);
            
            if (leveds_color == 1)                                               //  use all colors
            {
               lever  = fabsf(pixel0[0] - pixel1[0]) + fabsf(pixel0[0] - pixel2[0])
                      + fabsf(pixel0[0] - pixel3[0]) + fabsf(pixel0[0] - pixel4[0]);
               lever += fabsf(pixel0[1] - pixel1[1]) + fabsf(pixel0[1] - pixel2[1])
                      + fabsf(pixel0[1] - pixel3[1]) + fabsf(pixel0[1] - pixel4[1]);
               lever += fabsf(pixel0[2] - pixel1[2]) + fabsf(pixel0[2] - pixel2[2])
                      + fabsf(pixel0[2] - pixel3[2]) + fabsf(pixel0[2] - pixel4[2]);
               lever = lever / 12.0;
            }
            else                                                                 //  use single color
            {
               ii = leveds_color - 2;
               lever  = fabsf(pixel0[ii] - pixel1[ii]) + fabsf(pixel0[ii] - pixel2[ii])
                      + fabsf(pixel0[ii] - pixel3[ii]) + fabsf(pixel0[ii] - pixel4[ii]);
               lever = lever / 4.0;
            }
         }

         lever = lever / 1000.0;                                                 //  scale 0.0 to 0.999
         lever = pow(lever,0.3);                                                 //  log scale: 0.1 >> 0.5, 0.8 >> 0.94

         ii = ipy * E1pxm->ww + ipx;                                             //  save lever for each pixel
         leveds_lever[ii] = lever;
      }
   }

   if (strmatchN(event,"b ",2)) {                                                //  button to move entire curve
      for (ii = 0; ii < sd->nap[0]; ii++) {
         px = sd->apx[0][ii];
         py = sd->apy[0][ii];
         if (strmatch(event,"b +++")) py += 0.1;
         if (strmatch(event,"b ---")) py -= 0.1;
         if (strmatch(event,"b +-"))  py += 0.1 - 0.2 * px;
         if (strmatch(event,"b -+"))  py -= 0.1 - 0.2 * px;
         if (strmatch(event,"b +-+")) py -= 0.05 - 0.2 * fabsf(px-0.5);
         if (strmatch(event,"b -+-")) py += 0.05 - 0.2 * fabsf(px-0.5);
         if (py > 1) py = 1;
         if (py < 0) py = 0;
         sd->apy[0][ii] = py;
      }
      event = "edit";
   }

   if (strmatch(event,"edit")) {
      splcurve_generate(sd,0);                                                   //  regenerate the curve
      gtk_widget_queue_draw(sd->drawarea);
   }

   if (! CEF) return 1;                                                          //  edit canceled                      15.08

   for (int ipy = 1; ipy < E1pxm->hh-1; ipy++)
   for (int ipx = 1; ipx < E1pxm->ww-1; ipx++)
   {
      pixel0 = PXMpix(E1pxm,ipx,ipy);                                            //  target pixel to measure
      ii = ipy * E1pxm->ww + ipx;
      lever = leveds_lever[ii];                                                  //  leverage to apply, 0.0 to 0.999
      xval = lever;                                                              //  curve x-value, 0.0 to 0.999
      kk = 1000 * xval;
      if (kk > 999) kk = 999;
      yval = sd->yval[0][kk];                                                    //  y-value, 0 to 0.999
      pixdist = 1 + 998 * yval;                                                  //  pixel edge distance, 1 to 999
      sa_pixmap[ii] = pixdist;
   }

   if (CEF->zd) zdialog_send_event(CEF->zd,"blendwidth");                        //  notify edit dialog
   return 1;
}


//  this function is called when curve is edited using mouse

void  leveds_curve_update(int)
{
   if (! zdsela) return;
   leveds_event(zdsela,"edit");
   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 *)                                             //  new     15.01
{
   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;

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

   snprintf(plugfile,200,"%s/plugins",get_zuserdir());                           //  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, ZTX("Edit Plugins"),                         //  1st entry is Edit Plugins
                     m_edit_plugins, 0, ZTX("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,
                                 ZTX("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       15.01
{
   using namespace plugins_names;

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

   int         ii;
   char        *pp;
   zdialog     *zd;

   F1_help_topic = "plugins";

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

***/

   zd = zdialog_new(ZTX("Edit Plugins"),Mwin,Badd,Bremove,Bdone,null);
   zdialog_add_widget(zd,"hbox","hbm","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labm","hbm",ZTX("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",ZTX("command"),"space=5");
   zdialog_add_widget(zd,"entry","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_run(zd,edit_plugins_event);
   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,"escape")) zd->zstat = 4;                                  //  escape = cancel                    15.07

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

   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","");
   }

   if (zstat == 3)                                                               //  done
   {
      snprintf(buff,199,"%s/plugins",get_zuserdir());                            //  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;
}


//  process plugin menu selection
//  execute correspinding 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",tempdir);                           //  /.../fotoxx-nnnnnn/plugfile.tif

   err = PXM_TIFF_save(E1pxm,pluginfile,8);                                      //  E1 >> plugin_file
   if (err) {
      if (*file_errmess)                                                         //  pass error to user
         zmessageACK(Mwin,file_errmess);
      goto FAIL;
   }

   zd = zmessage_post(Mwin,0,ZTX("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,ZTX("plugin failed"));
      goto FAIL;
   }

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

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

FAIL:
   edit_cancel(0);

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



