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

   Fotoxx      edit photos and manage collections

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

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

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

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

   Fotoxx image edit - Tools menu functions

   m_index                 dialog to create/update image index file
   index_rebuild           create/update image index file
   m_quick_index           quick incremental index rebuild, no UI 
   index_rebuild_old       use old image index file without updates
   m_move_fotoxx_home      move fotoxx home folder from default ~/.fotoxx
   m_preferences           user preferences and settings dialog
   m_KBshortcuts           edit keyboard shortcuts
   KBshortcuts_load        load KB shortcuts file into memory
   m_RGB_dist              show RGB brightness distribution graph
   m_magnify               magnify the image within a radius of the mouse
   m_duplicates            find all duplicated images
   m_show_RGB              show RGB values for clicked image positions
   m_color_profile         convert from one color profile to another
   m_calibrate_printer     printer color calibration
   print_calibrated        print current image file with calibrated colors
   m_gridlines             setup for grid lines
   m_line_color            choose color for foreground lines
   m_darkbrite             highlight darkest and brightest pixels
   m_map_pixel_bias        map raw pixel bias (camera sensor), save to file
   pixel_bias_map_load     load pixel bias data from a file
   pixel_bias_fix          correct pixel bias in an image
   m_map_dead_pixels       map raw dead pixels (camera sensor), save to file
   dead_pixels_map_load    load dead pixels data from a file
   dead_pixels_fix         fix dead pixels in an image
   m_monitor_color         monitor color and contrast check
   m_monitor_gamma         monitor gamma check and adjust
   m_change_lang           choose GUI language
   m_untranslated          report missing translations
   m_phone_home_allow      allow/block anonymous usage statistics
   m_resources             print memory allocated and CPU time used
   m_appimage_files        show files included in appimage container
   m_zappcrash             zappcrash test


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

#define EX extern                                                                //  enable extern declarations
#include "fotoxx.h"

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

//  Index Image Files menu function
//  Dialog to get top image folders, thumbnails folder, indexed metadata items.
//  Update the index_config file and generate new image index.

namespace index_names
{
   zdialog  *zd_indexlog = 0;
   xxrec_t  *xxrec = 0, **xxrec_old = 0, **xxrec_new = 0;
   int      Xthread1, Xthread2, Tthread1, Tthread2; 
   int      Ffullindex = 0;                                                      //  user: force full re-index
   int      Fuserkill;
}


//  index menu function

void m_index(GtkWidget *, cchar *)
{
   using namespace index_names;

   void index_callbackfunc(GtkWidget *widget, int line, int pos, int kbkey);
   int index_dialog_event(zdialog *zd, cchar *event);

   zdialog        *zd;
   FILE           *fid;
   char           filespec[200], buff[200], sthumbfolder[200];
   char           *pp;
   GtkWidget      *widget;
   int            line, ii, cc, zstat;
   cchar          *greet1 = E2X("Folders for image files "
                                "(subfolders included automatically).");
   cchar          *greet2 = E2X("Select to add, click on X to delete.");
   cchar          *greet3 = E2X("folder for thumbnails");
   cchar          *greet4 = E2X("extra metadata items to include in index");
   cchar          *greet5 = E2X("force a full re-index of all image files");
   cchar          *termmess = E2X("Index function terminated. \n" 
                                  "Indexing is required for search and map functions \n"
                                  "and to make the gallery pages acceptably fast.");

   F1_help_topic = "index files";
   m_viewmode(0,"0");                                                            //  no view mode                       19.0

   if (checkpend("all")) return;                                                 //  check nothing pending
   Fblock = 1;
   Ffullindex = 0;

/***
       ______________________________________________________________
      |                      Index Image Files                       |
      |                                                              |
      | Folders for image files (subdirs included automatically).    |
      | [Select] Select to add, click on X to delete.                |
      |  __________________________________________________________  |
      | | X  /home/<user>/Pictures                                 | |
      | | X  /home/<user>/...                                      | |
      | |                                                          | |
      | |                                                          | |
      | |                                                          | |
      | |__________________________________________________________| |
      |                                                              |
      |  [Select] folder for thumbnails ___________________________  |
      | |__________________________________________________________| |
      |                                                              |
      | [Select] extra metadata items to include in index            |
      |                                                              |
      | [x] force a full re-index of all image files                 |
      |                                                              |
      |                                    [Help] [Proceed] [Cancel] |
      |______________________________________________________________|

***/

   zd = zdialog_new(E2X("Index Image Files"),Mwin,Bhelp,Bproceed,Bcancel,null);

   zdialog_add_widget(zd,"hsep","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbgreet1","dialog");
   zdialog_add_widget(zd,"label","labgreet1","hbgreet1",greet1,"space=3");
   zdialog_add_widget(zd,"hbox","hbtop","dialog");
   zdialog_add_widget(zd,"button","browsetop","hbtop",Bselect,"space=3");        //  browse top button
   zdialog_add_widget(zd,"label","labgreet2","hbtop",greet2,"space=5");

   zdialog_add_widget(zd,"hbox","hbtop2","dialog",0,"expand");
   zdialog_add_widget(zd,"label","space","hbtop2",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbtop2","hbtop2",0,"expand");
   zdialog_add_widget(zd,"label","space","hbtop2",0,"space=3");
   zdialog_add_widget(zd,"frame","frtop","vbtop2",0,"expand");
   zdialog_add_widget(zd,"scrwin","scrtop","frtop",0,"expand");
   zdialog_add_widget(zd,"text","topfolders","scrtop");                          //  topfolders text

   zdialog_add_widget(zd,"hsep","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbthumb1","dialog");
   zdialog_add_widget(zd,"button","browsethumb","hbthumb1",Bselect,"space=3");   //  browse thumb button
   zdialog_add_widget(zd,"label","labgreet3","hbthumb1",greet3,"space=5");
   zdialog_add_widget(zd,"hbox","hbthumb2","dialog");
   zdialog_add_widget(zd,"frame","frthumb","hbthumb2",0,"space=5|expand");
   zdialog_add_widget(zd,"zentry","sthumbfolder","frthumb");                     //  thumbnail folder

   zdialog_add_widget(zd,"hsep","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbxmeta","dialog");
   zdialog_add_widget(zd,"button","browsxmeta","hbxmeta",Bselect,"space=3");     //  browse xmeta metadata
   zdialog_add_widget(zd,"label","labgreet4","hbxmeta",greet4,"space=5");

   zdialog_add_widget(zd,"hsep","space","dialog",0,"space=5");                   //  force full re-index
   zdialog_add_widget(zd,"hbox","hbforce","dialog");
   zdialog_add_widget(zd,"check","forcex","hbforce",greet5,"space=3");
   
   widget = zdialog_widget(zd,"topfolders");                                     //  set mouse/KB event function
   textwidget_set_eventfunc(widget,index_callbackfunc);                          //    for top folders text window

   textwidget_clear(widget);                                                     //  default top folder
   textwidget_append(widget,0," X  %s\n",getenv("HOME"));                        //    /home/<user>

   snprintf(sthumbfolder,200,"%s/thumbnails",get_zhomedir());                    //  default thumbnails folder
   zdialog_stuff(zd,"sthumbfolder",sthumbfolder);                                //    /home/<user>/.fotoxx/thumbnails
   
   xmeta_keys[0] = 0;                                                            //  default no indexed metadata

   snprintf(filespec,200,"%s/index_config",index_folder);                        //  read index_config file,
                                                                                 //    stuff data into dialog widgets
   fid = fopen(filespec,"r");
   if (fid) {
      textwidget_clear(widget);
      while (true) {
         pp = fgets_trim(buff,200,fid,1);
         if (! pp) break;
         if (strmatchN(buff,"thumbnails:",11)) {                                 //  if "thumbnails: /..."
            if (buff[12] == '/')                                                 //    stuff thumbnails folder
               zdialog_stuff(zd,"sthumbfolder",buff+12);
         }
         else if (strmatchN(buff,"metadata:",9)) {                               //  if "metadata:"
            for (ii = 0; ii < Mxmeta; ii++) {                                    //    build indexed metadata list
               pp = (char *) strField(buff+9,"^",ii+1);
               if (! pp) break;
               xmeta_keys[ii] = zstrdup(pp);
            }
            xmeta_keys[ii] = 0;                                                  //  mark EOL
         }
         else textwidget_append(widget,0," X  %s\n",buff);                       //  stuff " X  /dir1/dir2..."
      }
      fclose(fid);
   }

   zdialog_resize(zd,500,500);                                                   //  run dialog
   zdialog_run(zd,index_dialog_event,"save");
   zstat = zdialog_wait(zd);                                                     //  wait for completion
   
   while (zstat == 1) {
      zd->zstat = 0;                                                             //  keep dialog active
      m_help(0,"Help");
      zstat = zdialog_wait(zd);
   }

   if (zstat != 2)                                                               //  canceled
   {
      zdialog_free(zd);
      zmessageACK(Mwin,termmess);                                                //  index not finished
      Fblock = 0;
      return;
   }
   
   snprintf(filespec,200,"%s/index_config",index_folder);                        //  open/write index config file
   fid = fopen(filespec,"w");
   if (! fid) {                                                                  //  fatal error
      zmessageACK(Mwin,"index_config file: \n %s",strerror(errno));
      Fblock = 0;
      quitxx();                                                                  //  unconditional exit                 20.05
   }

   widget = zdialog_widget(zd,"topfolders");                                     //  get top folders from dialog widget

   for (line = 0; ; line++) {
      pp = textwidget_line(widget,line,1);                                       //  loop widget text lines
      if (! pp || ! *pp) break;
      pp += 4;                                                                   //  skip " X  "
      if (*pp != '/') continue;
      strncpy0(buff,pp,200);                                                     //  /dir1/dir2/...
      cc = strlen(buff);
      if (cc < 5) continue;                                                      //  ignore blanks or rubbish
      if (buff[cc-1] == '/') buff[cc-1] = 0;                                     //  remove trailing '/'
      fprintf(fid,"%s\n",buff);                                                  //  write top folder to config file
   }

   zdialog_fetch(zd,"sthumbfolder",buff,200);                                    //  get thumbnails folder from dialog
   strTrim2(buff);                                                               //  remove surrounding blanks
   cc = strlen(buff);
   if (cc && buff[cc-1] == '/') buff[cc-1] = 0;                                  //  remove trailing '/'
   fprintf(fid,"thumbnails: %s\n",buff);                                         //  thumbnails folder >> config file

   *buff = 0;   
   for (ii = 0; ii < Mxmeta; ii++) {                                             //  indexed metadata >> config file 
      if (! xmeta_keys[ii]) break;
      strncatv(buff,200,xmeta_keys[ii],"^ ",0);
   }
   fprintf(fid,"metadata: %s\n",buff);

   fclose(fid);
   zdialog_free(zd);                                                             //  close dialog

   index_rebuild(2,1);                                                           //  build image index and thumbnail files
   if (! Findexvalid) m_index(0,0);                                              //  failed, try again
   Fblock = 0;                                                                   //  OK
   
   navi::galleryname = zstrdup(topfolders[0]);                                   //  set current gallery
   return;
}


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

//  mouse click function for top folders text window
//  remove folder from list where "X" is clicked

void index_callbackfunc(GtkWidget *widget, int line, int pos, int kbkey)
{
   GdkWindow   *gdkwin;
   char        *pp;
   char        *dirlist[maxtopfolders];
   int         ii, jj;
   
   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help 
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   gdkwin = gtk_widget_get_window(widget);                                       //  stop updates between clear and refresh
   gdk_window_freeze_updates(gdkwin);
   
   for (ii = jj = 0; ii < maxtopfolders; ii++)                                   //  loop text lines in widget
   {                                                                             //    " X  /dir1/dir2/... "
      pp = textwidget_line(widget,ii,1);
      if (! pp || strlen(pp) < 4) break;
      if (ii == line && pos < 3) continue;                                       //  if "X" clicked, skip deleted line
      dirlist[jj] = zstrdup(pp);
      jj++;
   }
   
   textwidget_clear(widget);

   for (ii = 0; ii < jj; ii++)                                                   //  stuff remaining lines back into widget
   {
      textwidget_append(widget,0,"%s\n",dirlist[ii]);
      zfree(dirlist[ii]); 
   }

   gdk_window_thaw_updates(gdkwin);
   return;
}


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

//  index dialog event and completion function

int index_dialog_event(zdialog *zd, cchar *event)
{
   using namespace index_names;

   int         ii, yn;
   GtkWidget   *widget;
   char        **flist, *pp, *sthumbfolder;
   cchar       *topmess = E2X("Choose top image folders");
   cchar       *thumbmess = E2X("Choose thumbnail folder");
   cchar       *xmetamess = E2X("All image files will be re-indexed. \n"
                                "  Continue?");

   if (strmatch(event,"browsetop")) {                                            //  [browse] top folders
      flist = zgetfiles(topmess,MWIN,"folders",getenv("HOME"));                  //  get top folders from user
      if (! flist) return 1;
      widget = zdialog_widget(zd,"topfolders");                                  //  add to dialog list
      for (ii = 0; flist[ii]; ii++) {
         textwidget_append2(widget,0," X  %s\n",flist[ii]);                      //  " X  /dir1/dir2/..."
         zfree(flist[ii]);
      }
      zfree(flist);
   }

   if (strmatch(event,"browsethumb")) {                                          //  [browse] thumbnail folder
      pp = zgetfile(thumbmess,MWIN,"folder",getenv("HOME"));
      if (! pp) return 1;
      sthumbfolder = zstrdup(pp,12);
      if (! strstr(sthumbfolder,"/thumbnails"))                                  //  if not containing /thumbnails,
         strcat(sthumbfolder,"/thumbnails");                                     //    append /thumbnails
      zdialog_stuff(zd,"sthumbfolder",sthumbfolder);
      zfree(sthumbfolder);
      zfree(pp);
   }
   
   if (strmatch(event,"browsxmeta")) {                                           //  [select]
      yn = zmessageYN(Mwin,xmetamess);                                           //  add optional indexed metadata
      if (! yn) return 1;
      ii = select_meta_keys(xmeta_keys,1);
      if (ii) Ffullindex = 1;                                                    //  changes made, force full re-index
   }
   
   if (strmatch(event,"forcex")) {                                               //  force full re-index 
      zdialog_fetch(zd,"forcex",ii);
      if (ii) Ffullindex = 1;
   }

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

   if (zd->zstat == 1) {                                                         //  [help]
      zd->zstat = 0;
      showz_docfile(Mwin,"userguide","index_files");
      return 1;
   }

   return 1;                                                                     //  [proceed] or cancel status
}


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

//  set up a minimal index for user who does not want to index

void index_noindex()
{
   FILE     *fid;
   char     sthumbfolder[200], indexfile[200];
   
   topfolders[0] = zstrdup(getenv("HOME"));                                      //  default top image folder
   Ntopfolders = 1;                                                              //    /home/<user>

   snprintf(sthumbfolder,200,"%s/thumbnails",get_zhomedir());                    //  default thumbnails folder
   thumbfolder = zstrdup(sthumbfolder);                                          //    /home/<user>/.fotoxx/thumbnails
   
   snprintf(indexfile,200,"%s/index_config",index_folder);                       //  open/write index config file
   fid = fopen(indexfile,"w");
   if (! fid) {                                                                  //  fatal error
      zmessageACK(Mwin,"%s \n %s",indexfile,strerror(errno));
      quitxx();                                                                  //  unconditional exit                 20.05
   }
   
   fprintf(fid,"%s\n",topfolders[0]);                                            //  write top folder
   fprintf(fid,"thumbnails: %s\n",sthumbfolder);                                 //  write thumbnail folder
   fclose(fid);

   return;   
}


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

//  Rebuild the image index from the index config data. 
//  index level = 0/1/2  =  no index / old files only / old + new files
//  Called from main() when Fotoxx is started (index level from user setting)
//  Called from menu function m_index() (index level = 2)

void index_rebuild(int indexlev, int menu)                                       //  overhauled for removable storage   20.0
{
   using namespace index_names;

   void   index_rebuild_old();
   int    indexlog_dialog_event(zdialog *zd, cchar *event);
   int    index_compare(cchar *rec1, cchar *rec2);
   void   *index_thread(void *);
   void   *thumb_thread(void *);
   void * index_sync(void *arg);

   GtkWidget   *wlog;
   FILE        *fid;
   char        **topfol2, **misstop2;
   int         *topcc2, *misscc2;
   int         yn, ii, jj, nn;
   int         Ntop, Nthumb;
   int         ftf, NF, orec, orec2, nrec, xrec, comp;
   int         cc, cc1, cc2, err;
   int         Nt1, Nt2;
   xxrec_t     *xxrec, **xxrec_new = 0, **xxrec_old = 0;
   int         Nnew, Nold;
   int         Xupdates, Tupdates, Tdeletes;
   int         *Xstatus = 0, *Tstatus = 0;
   char        *pp, *pp1, *pp2;
   char        filespec[200], buff[XFCC+500];
   char        **flist, *file, *thumbfile;
   STATB       statb;
   double      startime, time0;

   cchar *indexmess = E2X("No image file index was found.\n"
                          "An image file index will be created.\n"
                          "Your image files will not be changed.\n" 
                          "This may need considerable time if you \n"
                          "have many thousands of image files.");

   cchar *indexerr2 = E2X("Thumbnails folder: %s \n"
                          "Please remove.");
   
   cchar *thumberr = E2X("Thumbnails folder: \n  %s \n"
                         "must be named .../thumbnails");
   
   cchar *duperr = E2X("Duplicate or nested folders: \n %s \n %s \n"
                       "Please remove.");
   
   startime = get_seconds();                                                     //  index function start time
   Findexvalid = 0;
   Fblock = 1;
   Fuserkill = 0;

   //  get current top image folders and thumbnails folder
   //  from /home/<user>/.fotoxx/image_index/index_config
   
   for (ii = 0; ii < Ntopfolders; ii++)                                          //  free prior
      zfree(topfolders[ii]);

   Ntop = Nthumb = 0;
   snprintf(filespec,200,"%s/index_config",index_folder);                        //  read index config file

   fid = fopen(filespec,"r");
   if (fid) {
      while (true) {                                                             //  get top image folders
         pp = fgets_trim(buff,200,fid,1);
         if (! pp) break;
         if (strmatchN(buff,"thumbnails: /",13)) {                               //  get thumbnails folder
            if (thumbfolder) zfree(thumbfolder);
            thumbfolder = zstrdup(buff+12);
            Nthumb++;
         }
         else if (strmatchN(buff,"metadata:",9)) {                               //  get indexed metadata keys
            for (ii = 0; ii < Mxmeta; ii++) {
               pp = (char *) strField(buff+9,"^",ii+1);
               if (! pp) {
                  xmeta_keys[ii] = 0;
                  break;
               }
               xmeta_keys[ii] = zstrdup(pp);
            }
         }
         else {
            topfolders[Ntop] = zstrdup(buff);
            if (++Ntop == maxtopfolders) break;
         }
      }
      fclose(fid);
   }
   
   Ntopfolders = Ntop;
   
   if (indexlev == 0)                                                            //  indexing is disabled               20.0
   {
      if (Nthumb != 1)                                                           //  no index_config file, or invalid
      {
         thumbfolder = zstrdup("",200);                                          //  set default thumbnails folder
         snprintf(thumbfolder,200,"%s/thumbnails",get_zhomedir());

         fid = fopen(filespec,"w");                                              //  create stump index config file
         if (! fid) {                                                            //  (thumbnails folder only)
            zmessageACK(Mwin,"index config file error: %s",strerror(errno));
            quitxx();
         }
         fprintf(fid,"thumbnails: %s\n",thumbfolder);
         fclose(fid);
      }

      err = stat(thumbfolder,&statb);                                            //  create thumbnails folder if needed
      if (err || ! S_ISDIR(statb.st_mode))
         err = shell_ack("mkdir -p -m 0750 \"%s\" ",thumbfolder);
      if (err) {
         zmessageACK(Mwin,"%s \n %s",thumbfolder,strerror(errno));
         quitxx();
      }

      Nthumb = 1;
      printz("thumbnails folder: \n");                                           //  print thumbnails folder
      printz(" %s \n",thumbfolder);

      Ntopfolders = 0;                                                           //  no top image folders
      printz("no image index: reports disabled \n");                             //  no image index
      Findexvalid = 0;
      Fblock = 0;
      return;
   }

   if (! Ntopfolders) {                                                          //  if nothing found, must ask user
      zmessageACK(Mwin,E2X("specify at least 1 top image folder"));
      goto cleanup;
   }

   if (Nthumb != 1) {                                                            //  0 or >1 thumbnail folders
      zmessageACK(Mwin,E2X("specify 1 thumbnail folder"));
      goto cleanup;
   }

   cc = strlen(thumbfolder) - 11 ;                                               //  check /thumbnails name 
   if (! strmatch(thumbfolder+cc,"/thumbnails")) {
      zmessageACK(Mwin,thumberr,thumbfolder);
      goto cleanup;
   }

   err = stat(thumbfolder,&statb);                                               //  create thumbnails folder if needed
   if (err || ! S_ISDIR(statb.st_mode))
      err = shell_ack("mkdir -p -m 0750 \"%s\" ",thumbfolder);                   //  use shell mkdir
   if (err) {
      zmessageACK(Mwin,"%s \n %s",thumbfolder,strerror(errno));
      goto cleanup;
   }
   
   for (ii = 0; ii < Ntopfolders; ii++) {                                        //  disallow top dir = thumbnail dir 
      if (strmatch(topfolders[ii],thumbfolder)) {
         zmessageACK(Mwin,indexerr2,topfolders[ii]);
         goto cleanup;
      }
   }

   for (ii = 0; ii < Ntopfolders; ii++)                                          //  check for duplicate folders
   for (jj = ii+1; jj < Ntopfolders; jj++)                                       //   or nested folders
   {
      cc1 = strlen(topfolders[ii]);
      cc2 = strlen(topfolders[jj]);
      if (cc1 <= cc2) {
         pp1 = zstrdup(topfolders[ii],2);
         pp2 = zstrdup(topfolders[jj],2);
      }
      else {
         pp1 = zstrdup(topfolders[jj],2);
         pp2 = zstrdup(topfolders[ii],2);
         cc = cc1;
         cc1 = cc2;
         cc2 = cc;
      }
      strcpy(pp1+cc1,"/");
      strcpy(pp2+cc2,"/");
      nn = strmatchN(pp1,pp2,cc1+1);
      zfree(pp1);
      zfree(pp2);
      if (nn) {
         zmessageACK(Mwin,duperr,topfolders[ii],topfolders[jj]);                 //  user must fix
         goto cleanup;
      }
   }

   for (ii = jj = Nmisstops = 0; ii < Ntopfolders; ii++)                         //  check top image folders
   {
      err = stat(topfolders[ii],&statb);
      if (err || ! S_ISDIR(statb.st_mode))                                       //  move missing top image folders
         misstops[Nmisstops++] = topfolders[ii];                                 //    into a separate list
      else topfolders[jj++] = topfolders[ii];                                    //  (poss. not mounted)
   }

   Ntopfolders = jj;                                                             //  valid top image folders
   if (! Ntopfolders) {                                                          //  none, must ask user
      zmessageACK(Mwin,indexmess);
      goto cleanup;
   }

   topfol2 = (char **) zmalloc(Ntopfolders * sizeof(char *));                    //  top folders with '/' appended
   topcc2 = (int *) zmalloc(Ntopfolders * sizeof(int));                          //  cc of same
   misstop2 = (char **) zmalloc(Nmisstops * sizeof(char *));                     //  missing top folders with '/'
   misscc2 = (int *) zmalloc(Nmisstops * sizeof(int));                           //  cc of same

   for (ii = 0; ii < Ntopfolders; ii++) {                                        //  save top folders with appended '/'
      topfol2[ii] = zstrdup(topfolders[ii],2);                                   //    for use with later comparisons
      strcat(topfol2[ii],"/");
      topcc2[ii] = strlen(topfol2[ii]);
   }
   
   for (ii = 0; ii < Nmisstops; ii++) {
      misstop2[ii] = zstrdup(misstops[ii],2);
      strcat(misstop2[ii],"/");
      misscc2[ii] = strlen(misstop2[ii]);
   }

   printz("top image folders: \n"); 
   for (ii = 0; ii < Ntopfolders; ii++)                                          //  print top folders
      printz(" %s\n",topfolders[ii]);
   
   if (Nmisstops)
   for (ii = 0; ii < Nmisstops; ii++)                                            //  print missing top folders
      printz(" %s *** not mounted *** \n",misstops[ii]);

   printz("thumbnails folder: \n");                                              //  print thumbnails folder
   printz(" %s \n",thumbfolder);

   if (indexlev == 1) {
      printz("old image index: reports will omit new files \n");                 //  image index has old files only
      index_rebuild_old();
      Fblock = 0;
      return;
   }

   if (indexlev == 2)                                                            //  update image index for all image files
      printz("full image index: reports will be complete \n");

   //  create log window for reporting status and statistics
   
   if (zd_indexlog) zdialog_free(zd_indexlog);                                   //  make dialog for output log
   zd_indexlog = zdialog_new(E2X("build index"),Mwin,Bdone,Bcancel,null);
   zdialog_add_widget(zd_indexlog,"frame","frame","dialog",0,"expand");
   zdialog_add_widget(zd_indexlog,"scrwin","scrwin","frame");
   zdialog_add_widget(zd_indexlog,"text","text","scrwin");
   wlog = zdialog_widget(zd_indexlog,"text");

   zdialog_resize(zd_indexlog,700,500);
   zdialog_run(zd_indexlog,indexlog_dialog_event,0);         

   textwidget_append(wlog,0,"top image folders:\n");                             //  log top image folders
   for (ii = 0; ii < Ntopfolders; ii++) 
      textwidget_append(wlog,0," %s\n",topfolders[ii]);

   if (Nmisstops)                                                                //  log missing top image folders
   for (ii = 0; ii < Nmisstops; ii++)
      textwidget_append(wlog,0," %s *** not mounted *** \n",misstops[ii]);
   
   textwidget_append(wlog,0,"thumbnails folder: \n");                            //  log thumbnails folder
   textwidget_append(wlog,0," %s \n",thumbfolder);
   textwidget_scroll(wlog,-1);
   
   if (Ffullindex) {                                                             //  user: force full re-index
      Nold = 0;
      goto get_new_files;
   }

   //  read old image index file and build "old list" of index recs

   textwidget_append2(wlog,0,"reading image index file ...\n");

   cc = maximages * sizeof(xxrec_t *);
   xxrec_old = (xxrec_t **) zmalloc(cc);                                         //  "old" image index recs
   Nold = 0;
   ftf = 1;
   
   while (true)
   {
      xxrec = read_xxrec_seq(ftf);                                               //  read curr. index recs
      if (! xxrec) break;

      err = stat(xxrec->file,&statb);                                            //  file status
      if (err) {                                                                 //  file is missing
         for (ii = 0; ii < Nmisstops; ii++)                                      
            if (strmatchN(xxrec->file,misstop2[ii],misscc2[ii])) break;          //  within missing top folders?
         if (ii == Nmisstops) continue;                                          //  no, exclude file
      }
      else {                                                                     //  file is present
         if (! S_ISREG(statb.st_mode)) continue;                                 //  not a regular file, remove
         for (ii = 0; ii < Ntopfolders; ii++)
            if (strmatchN(xxrec->file,topfol2[ii],topcc2[ii])) break;            //  within top folders?
         if (ii == Ntopfolders) continue;                                        //  no, exclude file
      }

      xxrec_old[Nold] = xxrec;                                                   //  file is included
      if (++Nold == maximages) {
         zmessageACK(Mwin,"exceeded max. images: %d \n",maximages);
         Fblock = 0;
         quitxx();                                                               //  unconditional exit                 20.05
      }
   }

   textwidget_append2(wlog,0,"index files found: %d \n",Nold);
   printz("index files found: %d \n",Nold);
   
   //  sort old index recs in order of file name and file mod date

   if (Nold > 1)
      HeapSort((char **) xxrec_old,Nold,index_compare);

   //  replace older recs with newer recs that were appended to the file
   //    and are now sorted in file name sequence

   if (Nold > 1)
   {
      for (orec = 0, orec2 = 1; orec2 < Nold; orec2++)
      {
         if (strmatch(xxrec_old[orec]->file,xxrec_old[orec2]->file)) 
            xxrec_old[orec] = xxrec_old[orec2];
         else {
            orec++;
            xxrec_old[orec] = xxrec_old[orec2];
         }
      }

      Nold = orec + 1;                                                           //  new count
   }
   
get_new_files:

   //  find all image files and create "new list" of index recs

   textwidget_append2(wlog,0,"find all image files ...\n");

   cc = maximages * sizeof(xxrec_t *);
   xxrec_new = (xxrec_t **) zmalloc(cc);                                         //  "new" image index recs
   Nnew = 0;
   
   for (ii = 0; ii < Ntopfolders; ii++)
   {
      err = find_imagefiles(topfolders[ii],1+16+32,flist,NF);                    //  image files, recurse folders,
      if (err) {                                                                 //    omit symlinks                    20.0
         zmessageACK(Mwin,"find_imagefiles() failure \n");
         Fblock = 0;
         quitxx();                                                               //  unconditional exit                 20.05
      }
      
      if (Nnew + NF > maximages) {
         zmessageACK(Mwin,"exceeded max. images: %d \n",maximages);
         Fblock = 0;
         quitxx();                                                               //  unconditional exit                 20.05
      }

      for (jj = 0; jj < NF; jj++)
      {
         file = flist[jj];
         nrec = Nnew++;
         xxrec_new[nrec] = (xxrec_t *) zmalloc(sizeof(xxrec_t));                 //  allocate xxrec
         xxrec_new[nrec]->file = file;                                           //  filespec
         stat(file,&statb);
         compact_time(statb.st_mtime,xxrec_new[nrec]->fdate);                    //  file mod date
      }                                                                          //  remaining data left null

      if (flist) zfree(flist);
   }

   textwidget_append2(wlog,0,"image files found: %d \n",Nnew);
   printz("image files found: %d \n",Nnew);
   
   if (Nnew == 0) {                                                              //  no images found 
      zmessageACK(Mwin,E2X("Top folders have no images"));
      goto cleanup;
   }

   //  warn if large index job is about to begin 

   if (Nold == 0 && ! menu) {                                                    //  only if not started by user menu   20.0
      yn = zmessageYN(Mwin,E2X("0 old files found, %d new files found.\n"
                               "A full re-index is required. Continue?"),Nnew);
      if (! yn) goto cleanup;
   }

   //  sort new index recs in order of file name and file mod date

   if (Nnew > 1)
      HeapSort((char **) xxrec_new,Nnew,index_compare);

   //  create image index table in memory

   if (xxrec_tab)
   {
      for (ii = 0; ii < Nxxrec; ii++)                                            //  free memory for old xxrec_tab
      {
         if (xxrec_tab[ii]->file) zfree(xxrec_tab[ii]->file);
         if (xxrec_tab[ii]->tags) zfree(xxrec_tab[ii]->tags);
         if (xxrec_tab[ii]->capt) zfree(xxrec_tab[ii]->capt);
         if (xxrec_tab[ii]->comms) zfree(xxrec_tab[ii]->comms);
         if (xxrec_tab[ii]->location) zfree(xxrec_tab[ii]->location);
         if (xxrec_tab[ii]->country) zfree(xxrec_tab[ii]->country);
         if (xxrec_tab[ii]->xmeta) zfree(xxrec_tab[ii]->xmeta);
         zfree(xxrec_tab[ii]);
      }
      
      zfree(xxrec_tab);
   }

   cc = maximages * sizeof(xxrec_t *);                                           //  make new table with max. capacity
   xxrec_tab = (xxrec_t **) zmalloc(cc);
   Nxxrec = 0;
   
   Xstatus = (int *) zmalloc(maximages);                                         //  1/2/3 = file missing/need-update/OK
   Tstatus = (int *) zmalloc(maximages);                                         //  1/2/3 = thumb missing/need-update/OK

   //  merge and compare old and new index recs
   
   nrec = orec = xrec = 0;
   
   while (true)                                                                  //  merge old and new index recs
   {
      if (nrec == Nnew && orec == Nold) break;                                   //  both EOF, done
      
      if (nrec < Nnew && orec < Nold)                                            //  if neither EOF, compare
         comp = strcmp(xxrec_old[orec]->file, xxrec_new[nrec]->file);
      else comp = 0;
      
      if (nrec == Nnew || comp < 0) {                                            //  old index rec has no match
         xxrec_tab[xrec] = xxrec_old[orec];                                      //  copy old index rec to output
         Xstatus[xrec] = 1;                                                      //  mark file as missing
         xxrec_old[orec] = 0;
         orec++;
      }
      
      else if (orec == Nold || comp > 0) {                                       //  new index rec has no match
         xxrec_tab[xrec] = xxrec_new[nrec];                                      //  copy new index rec to output
         Xstatus[xrec] = 2;                                                      //  mark file as needing update
         xxrec_new[nrec] = 0;
         nrec++;
      }
      
      else {                                                                     //  old and new index recs match
         xxrec_tab[xrec] = xxrec_old[orec];                                      //  copy old index rec to output
         if (strmatch(xxrec_old[orec]->fdate, xxrec_new[nrec]->fdate))           //  compare file dates
            Xstatus[xrec] = 3;                                                   //  same, mark file as up to date
         else {
            Xstatus[xrec] = 2;                                                   //  different, mark as needing update
            strcpy(xxrec_tab[xrec]->fdate, xxrec_new[nrec]->fdate);              //  use current file date
         }
         xxrec_old[orec] = 0;
         orec++;

         zfree(xxrec_new[nrec]->tags);                                           //  free memory for new index rec
         zfree(xxrec_new[nrec]->capt);
         zfree(xxrec_new[nrec]->comms);
         zfree(xxrec_new[nrec]->location);
         zfree(xxrec_new[nrec]->country);
         zfree(xxrec_new[nrec]->xmeta);

         xxrec_new[nrec] = 0;
         nrec++;
      }
      
      Tstatus[xrec] = 3;                                                         //  thumbnail OK
      if (Xstatus[xrec] > 1)                                                     //  if file not missing,
         if (! thumbfile_OK(xxrec_tab[xrec]->file))                              //    test and mark thumbnail
            Tstatus[xrec] = 2;
      
      xrec++;                                                                    //  count output recs
      if (xrec == maximages) {
         zmessageACK(Mwin,"max. image limit reached: %d",xrec);
         goto cleanup;
      }
   }
   
   Nxxrec = xrec;

   if (xxrec_new) zfree(xxrec_new);                                              //  free memory
   if (xxrec_old) zfree(xxrec_old);
   xxrec_new = xxrec_old = 0;

   for (ii = Xupdates = Tupdates = 0; ii < Nxxrec; ii++) {                       //  count updates required
      if (Xstatus[ii] == 2) Xupdates++;                                          //  index rec updates
      if (Tstatus[ii] == 2) Tupdates++;                                          //  thumbnail file updates
   }

   textwidget_append(wlog,0,"index updates needed: %d  thumbnails: %d \n",Xupdates,Tupdates); 
   printz("index updates needed: %d  thumbnails: %d \n",Xupdates,Tupdates);

   //  Process entries needing update in the new index table
   //  (new files or files dated later than in old index table).
   //  Get updated metadata from image file EXIF/IPTC data.
   //  Add missing or update stale thumbnail file.

   textwidget_append(wlog,0,"updating image index and thumbnails ... \n");
   textwidget_append2(wlog,0,"\n");
   printz("updating image index and thumbnails ... \n");

   Xupdates = Tupdates = Tdeletes = 0;
   Xthread1 = Xthread2 = 0;
   Tthread1 = Tthread2 = 0;

   Nt1 = 2; Nt2 = 4;                                                             //  no. parallel threads               19.20
   if (Ftinycomputer) Nt2 = 2;                                                   //  computer with inadequate memory    19.20

   for (xrec = 0; xrec < Nxxrec; xrec++)                                         //  loop all index recs
   {
      if (Fuserkill || Fshutdown) break;                                         //  killed by user 
      
      if (Xstatus[xrec] == 2) {                                                  //  update metadata
         Xupdates++;
         while (Xthread1 - Xthread2 == Nt1) zsleep(0.001);                       //  exif_server count (2)
         Xthread1++;
         Xstatus[xrec] = xrec;                                                   //  separate arg per thread
         start_detached_thread(index_thread,&Xstatus[xrec]);
      }

      if (Tstatus[xrec] == 2) {                                                  //  update thumbnail 
         Tupdates++;
         while (Tthread1 - Tthread2 == Nt2) zsleep(0.001);                       //  4 parallel threads                 19.20
         Tthread1++;                                                             //  (more not faster)
         Tstatus[xrec] = xrec;
         start_detached_thread(thumb_thread,&Tstatus[xrec]);
      }
      
      if (Xstatus[xrec] == xrec || Tstatus[xrec] == xrec) {                      //  update counters 
         file = xxrec_tab[xrec]->file;
         textwidget_replace(wlog,0,-1,"%d %d %s \n",Xupdates,Tupdates,file);
      }

      if (Xupdates && Xupdates == 2000 * (Xupdates / 2000))                      //  update image index file 
      {                                                                          //    every 2000 updated recs.
         ftf = 1;
         for (ii = 0; ii < xrec; ii++)
            write_xxrec_seq(xxrec_tab[ii],ftf);
         write_xxrec_seq(null,ftf);                                              //  close output
      }
   }

   time0 = get_seconds();

   while (true)                                                                  //  wait for last threads to exit
   {
      zsleep(0.01);
      if (get_seconds() - time0 > 20) {                                          //  limit the wait time
         ii = Xthread1 - Xthread2;
         if (ii) printz("*** %d index threads pending \n",ii);                   //  note the failure
         ii = Tthread1 - Tthread2;
         if (ii) printz("*** %d thumbnail threads pending \n",ii);
         break;
      }
      if (Xthread2 != Xthread1) continue;
      if (Tthread2 != Tthread1) continue;
      break;
   }
   
   if (Fuserkill || Fshutdown) goto cleanup;

   textwidget_replace(wlog,0,-1,"index updates: %d   thumbnails: %d \n",         //  final statistics
                           Xupdates, Tupdates);
   printz("index updates: %d  thumbnail updates: %d, deletes: %d \n",
                       Xupdates, Tupdates, Tdeletes);

   //  write updated index records to image index file
   
   printz("writing updated image index file \n");  
   
   if (Nxxrec)
   {
      err = 0;
      ftf = 1;
      for (xrec = 0; xrec < Nxxrec; xrec++) 
         write_xxrec_seq(xxrec_tab[xrec],ftf);
      write_xxrec_seq(null,ftf);                                                 //  close output
      start_detached_thread(index_sync,0);                                       //  commit index file to disk
   }

//  keep index records in memory only for files actually present

   textwidget_append2(wlog,0,"all image files, including unmounted folders: %d \n",Nxxrec);
   printz("all image files, including unmounted folders: %d \n",Nxxrec);

   for (ii = jj = 0; ii < Nxxrec; ii++)
   {
      err = stat(xxrec_tab[ii]->file,&statb);
      if (err) continue;
      xxrec_tab[jj++] = xxrec_tab[ii];
   }
   
   Nxxrec = jj;                                                                  //  new count

   textwidget_append2(wlog,0,"image files in currently mounted folders: %d \n",Nxxrec);
   printz("image files in currently mounted folders: %d \n",Nxxrec);
   
   //  find orphan thumbnails and delete them
   
   err = find_imagefiles(thumbfolder,2+16+32,flist,NF);                          //  thumbnails, recurse dirs, 
   if (err) {                                                                    //    ignore symlinks                  20.0
      zmessageACK(Mwin,strerror(errno));
      NF = 0;
   }
   
   textwidget_append2(wlog,0,"thumbnails found: %d \n",NF);
   printz("thumbnails found: %d \n",NF);

   textwidget_append2(wlog,0,"deleting orphan thumbnails ... \n");
   Tdeletes = 0;

   for (ii = 0; ii < NF; ii++)
   {
      zmainloop(100);
      thumbfile = flist[ii];
      file = thumb2imagefile(thumbfile);                                         //  thumbnail corresponding file
      if (file) {                                                                //  exists, keep thumbnail
         zfree(file); 
         zfree(thumbfile); 
         continue; 
      }
      
      pp = thumbfile + strlen(thumbfolder);                                      //  corresponding file within
      for (jj = 0; jj < Nmisstops; jj++)                                         //    a missing top image folder?
         if (strmatchN(misstop2[jj],pp,misscc2[jj])) break;
      if (jj < Nmisstops) continue;                                              //  yes, keep thumbnail

      remove(thumbfile);                                                         //  remove thumbnail file
      zfree(thumbfile);
      Tdeletes++;
   }

   if (flist) zfree(flist);
   
   textwidget_append2(wlog,0,"thumbnails deleted: %d \n",Tdeletes);
   printz("thumbnails deleted: %d \n",Tdeletes);

   textwidget_append2(wlog,0,"%s\n",Bcompleted);                                 //  index complete and OK
   printz("index time: %.1f seconds \n",get_seconds() - startime);               //  log elapsed time 

   if (Nxxrec) {
      Findexvalid = 2;                                                           //  image index is complete
      Findexlev = 2;                                                             //  fotoxx command: index new files    20.0
      FMindexlev = 1;                                                            //  file manager: old index only
   }

cleanup:                                                                         //  free allocated memory

   if (Fshutdown) return;                                                        //  process terminating

   if (xxrec_old) zfree(xxrec_old);
   if (xxrec_new) zfree(xxrec_new);
   xxrec_old = xxrec_new = 0;
   
   if (Xstatus) zfree(Xstatus);
   if (Tstatus) zfree(Tstatus);
   Xstatus = Tstatus = 0;

   if (zd_indexlog && ! menu)                                                    //  if not manual run, kill log window
      zdialog_send_event(zd_indexlog,"exitlog");
   
   Fblock = 0;                                                                   //  unblock
   Ffullindex = 0;                                                               //  full re-index done

   if (! Findexvalid) {
      if (menu) return;                                                          //  called manually
      else m_index(0,0);                                                         //  called from fotoxx startup
   }

   return;
}


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

//  thread process - call sync() asynchronously

void * index_sync(void *arg)                                                     //  20.0
{
   double  t0, t1;

   t0 = get_seconds();
   sync();
   t1 = get_seconds() - t0;
   printz("sync() after index build  %.3f \n",t1);
   pthread_exit(0);
}


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

//  thread process - create thumbnail file for given image file

void * thumb_thread(void *arg)
{
   using namespace index_names;

   int      xrec;
   char     *file;
   
   xrec = *((int *) arg);
   file = xxrec_tab[xrec]->file;                                                 //  image file to check
   update_thumbfile(file);                                                       //  do thumbnail update if needed
   zadd_locked(Tthread2,+1);                                                     //  completions
   pthread_exit(0);
}


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

//  thread process - get image file metadata and build image index rec.
//  thread has no GTK calls

void * index_thread(void *arg)
{
   using namespace index_names;

   int      xrec, err, ii, nkey, xcc, acc;
   int      ww, hh;
   char     *file, *pp;
   float    flati, flongi;
   char     xmetarec[1000];                                                      //  max. extra metadata, bytes
   STATB    statb;

   cchar    *exifkeys[100] = { "FileName", exif_date_key, iptc_keywords_key,     //  first 12 keys are fixed
            iptc_rating_key, exif_ww_key, exif_hh_key,                           //  (replace exif_wwhh_key)            19.15
            exif_comment_key, iptc_caption_key,
            exif_city_key, exif_country_key, exif_lati_key, exif_longi_key };

   char     *ppv[100], *exiffile = 0;
   char     *exifdate = 0, *iptctags = 0, *iptcrating = 0;
   char     *exifww = 0, *exifhh = 0, *iptccapt = 0, *exifcomms = 0;
   char     *exifcity = 0, *exifcountry = 0, *exiflat = 0, *exiflong = 0;
   
   xrec = *((int *) arg);
   file = xxrec_tab[xrec]->file;                                                 //  image file
   
   nkey = 12;                                                                    //  add keys for indexed metadata
   for (ii = 0; ii < Mxmeta; ii++) {                                             //    from exifkeys[12]
      if (! xmeta_keys[ii]) break;
      exifkeys[nkey] = xmeta_keys[ii];
      nkey++;
   }
   
   err = exif_get(file,exifkeys,ppv,nkey);                                       //  get exif/iptc metadata
   if (err) {
      printz("exif_get() failure: %s \n",file);                                  //  metadata unreadable
      goto exit_thread;
   }
   
   exiffile = ppv[0];                                                            //  base file name
   pp = strrchr(file,'/');
   if (! exiffile || ! strmatch(exiffile,pp+1)) {                                //  image file has no metadata
      printz("exif_get() no data: %s \n",file);
      goto exit_thread;
   }
   
   exifdate = ppv[1];                                                            //  exif/iptc metadata returned
   iptctags = ppv[2];                                                            //    12 fixed keys
   iptcrating = ppv[3];
   exifww = ppv[4];
   exifhh = ppv[5];
   exifcomms = ppv[6];
   iptccapt = ppv[7];
   exifcity = ppv[8];
   exifcountry = ppv[9];
   exiflat = ppv[10];
   exiflong = ppv[11];
   
   if (exifdate && strlen(exifdate) > 3)                                         //  exif date (photo date)
      exif_tagdate(exifdate,xxrec_tab[xrec]->pdate);

   if (iptcrating && strlen(iptcrating)) {                                       //  iptc rating '0' - '5'
      xxrec_tab[xrec]->rating[0] = *iptcrating;
      xxrec_tab[xrec]->rating[1] = 0;
   }
   else strcpy(xxrec_tab[xrec]->rating,"0");                                     //  not present
   
   if (exifww && exifhh) {                                                       //  19.15
      convSI(exifww,ww);
      convSI(exifhh,hh);
      if (ww > 0 && hh > 0) {
         xxrec_tab[xrec]->ww = ww;
         xxrec_tab[xrec]->hh = hh;
      }
   }

   err = stat(file,&statb);
   xxrec_tab[xrec]->fsize = statb.st_size;                                       //  file size                          19.0

   if (iptctags && strlen(iptctags)) {                                           //  iptc tags
      xxrec_tab[xrec]->tags = iptctags;
      iptctags = 0;
   }

   if (iptccapt && strlen(iptccapt)) {                                           //  iptc caption
      xxrec_tab[xrec]->capt = iptccapt;
      iptccapt = 0;
   }

   if (exifcomms && strlen(exifcomms)) {                                         //  exif comments
      xxrec_tab[xrec]->comms = exifcomms;
      exifcomms = 0;
   }
   
   if (exifcity && strlen(exifcity)) {
      xxrec_tab[xrec]->location = exifcity;
      exifcity = 0;
   }

   if (exifcountry && strlen(exifcountry)) {
      xxrec_tab[xrec]->country = exifcountry;
      exifcountry = 0;
   }

   if (exiflat && exiflong) {
      flati = atof(exiflat);
      flongi = atof(exiflong);
      if (flati < -90.0 || flati > 90.0) flati = flongi = 0;
      if (flongi < -180.0 || flongi > 180.0) flati = flongi = 0;
      xxrec_tab[xrec]->flati = flati;
      xxrec_tab[xrec]->flongi = flongi;
   }

   xcc = 0;

   for (ii = 12; ii < nkey; ii++)                                                //  extra indexed metadata if any
   {
      if (! ppv[ii]) continue;
      acc = strlen(exifkeys[ii]) + strlen(ppv[ii]) + 3;
      if (acc > 100 || acc + xcc > 998) {                                        //  limit each key < 100, all < 1000   20.0
         printz("indexed metadata too big, omitted: %s %s \n",exifkeys[ii],file);
         continue;
      }
      strcpy(xmetarec+xcc,exifkeys[ii]);                                         //  construct series 
      xcc += strlen(exifkeys[ii]);                                               //    "keyname=keydata^ "
      xmetarec[xcc++] = '=';
      strcpy(xmetarec+xcc,ppv[ii]);
      xcc += strlen(ppv[ii]); 
      strcpy(xmetarec+xcc,"^ ");
      xcc += 2;
      zfree(ppv[ii]);
   }

   if (xcc > 0) xxrec_tab[xrec]->xmeta = zstrdup(xmetarec);

exit_thread:

   if (! xxrec_tab[xrec]->pdate[0]) strcpy(xxrec_tab[xrec]->pdate,"null");       //  missing values are "null"
   if (! xxrec_tab[xrec]->tags) xxrec_tab[xrec]->tags = zstrdup("null");
   if (! xxrec_tab[xrec]->capt) xxrec_tab[xrec]->capt = zstrdup("null");
   if (! xxrec_tab[xrec]->comms) xxrec_tab[xrec]->comms = zstrdup("null");
   if (! xxrec_tab[xrec]->location) xxrec_tab[xrec]->location = zstrdup("null");
   if (! xxrec_tab[xrec]->country) xxrec_tab[xrec]->country = zstrdup("null");
   if (! xxrec_tab[xrec]->xmeta) xxrec_tab[xrec]->xmeta = zstrdup("null");

   if (exiffile) zfree(exiffile);
   if (exifdate) zfree(exifdate);                                                //  free EXIF data
   if (iptcrating) zfree(iptcrating);
   if (exifww) zfree(exifww);
   if (exifhh) zfree(exifhh);
   if (iptctags) zfree(iptctags);
   if (iptccapt) zfree(iptccapt);
   if (exifcomms) zfree(exifcomms);
   if (exifcity) zfree(exifcity);
   if (exifcountry) zfree(exifcountry);
   if (exiflat) zfree(exiflat);
   if (exiflong) zfree(exiflong);

   zadd_locked(Xthread2,+1);
   pthread_exit(0);
}


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

//  index log window dialog response function

int indexlog_dialog_event(zdialog *zd, cchar *event)
{
   using namespace index_names;

   int      nn;
   cchar    *canmess = E2X("Cancel image index function?");
   
   if (strmatch(event,"exitlog")) {                                              //  auto-kill from index_rebuild()
      zmainloop();
      zsleep(0.5);
      zdialog_free(zd);
      zd_indexlog = 0;
      return 1;
   }

   if (! zd->zstat) return 1;                                                    //  continue

   if (zd->zstat == 1 || zd->zstat == 2) {                                       //  [Done] or [Cancel] button
      if (Findexvalid) {     
         zdialog_free(zd);                                                       //  index completed, kill dialog
         zd_indexlog = 0;
         return 1;
      }
      nn = zdialog_choose(Mwin,"mouse",canmess,Bcontinue,Bcancel,null);          //  incomplete, ask for confirmation 
      if (nn == 1) {
         zd->zstat = 0;                                                          //  continue
         return 1;
      }
   }
   
   zdialog_free(zd);                                                             //  cancel, kill index job 
   zd_indexlog = 0;
   Fuserkill = 1;
   return 1;
}


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

//  sort compare function - compare index records and return
//    <0   0   >0   for   rec1 < rec2   rec1 == rec2   rec1 > rec2

int index_compare(cchar *rec1, cchar *rec2)
{
   xxrec_t *xxrec1 = (xxrec_t *) rec1;
   xxrec_t *xxrec2 = (xxrec_t *) rec2;

   int nn = strcmp(xxrec1->file,xxrec2->file);
   if (nn) return nn;
   nn = strcmp(xxrec1->fdate,xxrec2->fdate);
   return nn;
}


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

//  quick incremental index rebuild with no user interface

void m_quick_index(GtkWidget *, cchar *)                                         //  20.0
{
   char     *galleryname = 0;

   F1_help_topic = "quick index";
   
   if (checkpend("all")) return;                                                 //  check nothing pending

   if (FGWM == 'G') {
      if (navi::galleryname && navi::gallerytype == GDIR)                        //  save current gallery
         galleryname = zstrdup(navi::galleryname);
   }
   
   index_rebuild(2,0);                                                           //  run incremental index

   if (galleryname) {
      gallery(galleryname,"init",0);                                             //  restore gallery and scroll position
      gallery_memory("get");
      gallery(0,"paint",-1);
      m_viewmode(0,"G");
   }

   return;
}


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

//  Rebuild image index table from existing image index file 
//    without searching for new and modified files.

void index_rebuild_old()
{
   using namespace index_names;

   int      ftf, cc, err, rec, rec2, Ntab;
   STATB    statb;
   
   Fblock = 1;
   Findexvalid = 0;

   //  read image index file and build table of index records

   cc = maximages * sizeof(xxrec_t *);
   xxrec_tab = (xxrec_t **) zmalloc(cc);                                         //  image index recs
   Ntab = 0;
   ftf = 1;

   while (true)
   {
      xxrec = read_xxrec_seq(ftf);                                               //  read curr. index recs
      if (! xxrec) break;
      err = stat(xxrec->file,&statb);
      if (err) continue;                                                         //  skip missing files                 20.0
      if (! S_ISREG(statb.st_mode)) continue;
      xxrec_tab[Ntab] = xxrec;
      Ntab++;
      if (Ntab == maximages) {
         zmessageACK(Mwin,"exceeded max. images: %d \n",maximages);
         Fblock = 0;
         quitxx();                                                               //  unconditional exit                 20.05
      }
      zmainloop(100);
   }
   
   //  sort index recs in order of file name and file mod date

   if (Ntab)
      HeapSort((char **) xxrec_tab,Ntab,index_compare);

   //  replace older recs with newer (appended) recs now sorted together

   if (Ntab)
   {   
      for (rec = 0, rec2 = 1; rec2 < Ntab; rec2++)
      {
         if (strmatch(xxrec_tab[rec]->file,xxrec_tab[rec2]->file)) 
            xxrec_tab[rec] = xxrec_tab[rec2];
         else {
            rec++;
            xxrec_tab[rec] = xxrec_tab[rec2];
         }
      }

      Ntab = rec + 1;                                                            //  new count
   }

   Nxxrec = Ntab;
   if (Nxxrec) Findexvalid = 1;                                                  //  index OK but missing new files
   Fblock = 0;                                                                   //  unblock
   return;
}


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

//  move fotoxx home folder with all contained user files
//  does not return. starts new session if OK.

void m_move_fotoxx_home(GtkWidget *, cchar *) 
{
   int      yn, nn, err;
   FILE     *fid;
   STATB    statb;
   char     oldhome[200], newhome[200];
   char     oldstring[200], newstring[200];
   char     *pp, configfile[200], buff[200], fotoxxhome[100];

   cchar    *mess1 = E2X("Do you want to move Fotoxx home? \n"
                         "  from: %s \n  to: %s");
   cchar    *mess3 = E2X("moving files ...");

   F1_help_topic = "move fotoxx home";

   if (checkpend("all")) return;                                                 //  check nothing pending

   strncpy0(oldhome,get_zhomedir(),200);                                         //  get present home folder

   pp = oldhome + strlen(oldhome);                                               //  remove trailing '/' if present
   if (pp[-1] == '/') pp[-1] = 0;

   err = stat(oldhome,&statb);                                                   //  check old home exists
   if (err) {
      zmessageACK(Mwin,"old fotoxx home: %s \n",strerror(errno));
      zexit("exit fotoxx");
   }

   if (! S_ISDIR(statb.st_mode)) {                                               //  check if a folder
      zmessageACK(Mwin,"old location is not a folder");
      zexit("exit fotoxx");
   }

retry:

   pp = zgetfolder(E2X("new Fotoxx home folder"),MWIN,oldhome);                  //  get new home folder
   if (! pp) return;
   strncpy0(newhome,pp,200);
   zfree(pp);

   pp = newhome + strlen(newhome);                                               //  remove trailing '/' if present
   if (pp[-1] == '/') pp[-1] = 0;
   
   err = stat(newhome,&statb);                                                   //  check new home folder exists
   if (err) {
      zmessageACK(Mwin,strerror(errno));
      goto retry;
   }
   
   if (! S_ISDIR(statb.st_mode)) {                                               //  check if a folder
      zmessageACK(Mwin,E2X("new location is not a folder"));
      goto retry;
   }
   
   if (strchr(newhome,' ')) {                                                    //  disallow blanks in path name
      zmessageACK(Mwin,E2X("new location name contains a space"));
      goto retry;
   }
   
   if (! strcasestr(newhome,"fotoxx"))                                           //  add /fotoxx if not already         19.0
      strncatv(newhome,200,"/fotoxx",null);
   
   if (strmatch(oldhome,newhome)) goto retry;
   
   yn = zmessageYN(Mwin,mess1,oldhome,newhome);                                  //  ask user to proceed
   if (! yn) goto retry;
   
   printz("%s \n",mess3);                                                        //  "moving files ..."
   poptext_mouse(mess3,0,0,0,0);

   err = shell_ack("cp -R %s/* %s",oldhome,newhome);                             //  copy all files from old home to new
   if (err) {
      zmessageACK(Mwin,"copy failed: %s \n",strerror(errno));
      zexit("exit fotoxx");
   }
   
   zmainsleep(2);
   poptext_mouse(0,0,0,0,0);

   snprintf(configfile,200,"%s/image_index/index_config",newhome);               //  read (new) index config file
   fid = fopen(configfile,"r");
   if (! fid) {
      zmessageACK(Mwin,"index_config file: %s",strerror(errno));
      m_quit(0,0);
   }
   
   while (true) {
      pp = fgets_trim(buff,200,fid,1);                                           //  look for record "thumbnails: /..."
      if (! pp) break;
      if (strmatchN(pp,"thumbnails: /",13)) break;
   }
   
   fclose(fid);

   if (! pp) {
      zmessageACK(Mwin,E2X("index_config file has no thumbnails folder"));
      m_quit(0,0);
   }
   
   snprintf(oldstring,200,"thumbnails: %s/thumbnails",oldhome);                  //  if thumbnails folder inside,
   snprintf(newstring,200,"thumbnails: %s/thumbnails",newhome);                  //    oldhome, change to newhome
   nn = zsed(configfile,oldstring,newstring,null);
   if (nn < 0) {
      zmessageACK(Mwin,E2X("update thumbnail folder failed"));
      m_quit(0,0);
   }
   if (nn == 0) zmessageACK(Mwin,E2X("thumbnails folder not changed"));
   else zmessageACK(Mwin,E2X("new thumbnails folder: %s/thumbnails"),newhome);
   
   snprintf(fotoxxhome,100,"%s/.fotoxx-home",getenv("HOME"));                    //  write new ~/.fotoxx-home file
   fid = fopen(fotoxxhome,"w");
   if (! fid) {
      zmessageACK(Mwin,"cannot write .fotoxx-home file: %s",strerror(errno));
      m_quit(0,0);
   }
   fprintf(fid,"%s",newhome);
   fclose(fid);
   
   zmessageACK(Mwin,E2X("Fotoxx will restart"));
   new_session(0);                                                               //  start fotoxx again
   zsleep(1);                                                                    //  delay before SIGTERM in quitxx()   19.0
   quitxx();                                                                     //  exit this session

   return;
}


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

//  user preferences and settings dialog

namespace preferences
{
   cchar    *startopt[8][2] = {  
               { "recent", E2X("Recent Files Gallery") },                        //  fotoxx startup display options
               { "newest", E2X("Newest Files Gallery") },
               { "specG",  E2X("Specific Gallery") },
               { "album",  E2X("Album Gallery") },
               { "prevG",  E2X("Previous Gallery") },
               { "prevF",  E2X("Previous File") },
               { "specF",  E2X("Specific File") },
               { "blank",  E2X("Blank Window") } };
   
   cchar    *tiffopt[4][2] = {                                                   //  TIFF file compression options      20.0
               { "NONE", "1" },
               { "LZW", "5" },
               { "PACKBITS", "32773" },
               { "DEFLATE", "8" } };
   int      NTO = 4;

   char     *choosefile = 0;                                                     //  file/gallery/album to display
}


//  menu function

void m_preferences(GtkWidget *, cchar *)
{
   using namespace preferences;
   
   int   preferences_dialog_event(zdialog *zd, cchar *event);

   int      ii, zstat;
   zdialog  *zd;
   char     txrgb[20], pct_scale[40]; 
   
   snprintf(pct_scale,40,E2X("%c of scale"),'%');                                //  prepare text "% of scale"          20.0
   

/***
       _________________________________________________________________
      |                     Preferences and Settings                    |
      |                                                                 |
      | Startup Display:  [________________|v] [browse]                 |        (popup file chooser)
      | Background Colors:  F-view [###]  G-view [###]                  |
      | Menu Style:  (o) Icons  (o) Text  (o) Both  Icon size [__]      |
      | Menu Colors:  Text [###]  Background [###]                      |
      | Dialog Font:  [ Ubuntu Bold 11   ] [choose]                     |        (popup font chooser)
      | Image Zoom Speed:  [ 2 ] zooms per 2x size increase             |
      | Image Pan Mode:  (o) drag  (o) scroll  [x] fast                 |
      | JPEG file save quality:  [ 90 ]  (90 = high quality)            |
      | TIFF file compression method [_1_]                              |        20.0
      | PNG file compression level [_1_]                                |        20.0
      | Curve Node Separation, Capture Range:  [ 5 ] % of scale         |
      | Map Marker Size:  [ 8 ] pixels                                  |
      | Show Images:  (o) last version only  (o) all images             |
      | Image Position in Window:  (o) centered  (o) right side         |
      | Image Index Level:                                              |
      |     [ 2 ] Fotoxx started directly (2)                           |        0/1/2 = none/old/old+search new
      |     [ 1 ] Fotoxx started by file manager (1)                    |
      | RAW File Loader:  (o) dcraw  (o) RawTherapee                    |        20.0
      | RAW Conversion Options:                                         |        20.0
      |     [x] extend dynamic range for dim images                     |
      |     [x] use embedded image as a guide                           |
      | RAW File Types: [ .arw .srf .sr2 .crw .cr2 .cr3 .dng .mdc ]     |
      | Video File Types: [ .mp4 .mov .avi .wmv .mpeg .h264 .webm ]     |
      | Video File Play Command: [ ffplay -loglevel 8 -autoexit |v]     |        shell command  20.0
      |                                                                 |
      |                                                 [done] [cancel] |
      |_________________________________________________________________|
         
***/         

   F1_help_topic = "preferences";

   m_viewmode(0,"0");                                                            //  file view mode
   if (checkpend("all")) return;                                                 //  check nothing pending
   Fblock = 1;

   zd = zdialog_new(E2X("Preferences and Settings"),Mwin,Bdone,Bcancel,null);

   zdialog_add_widget(zd,"hbox","hbstart","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labstart","hbstart",E2X("Startup Display:"),"space=3");
   zdialog_add_widget(zd,"label","space","hbstart",0,"space=5");
   zdialog_add_widget(zd,"combo","startopt","hbstart",0,"space=5");
   zdialog_add_widget(zd,"button","browse","hbstart",Bbrowse,"space=5");
   
   zdialog_add_widget(zd,"hbox","hbbg","dialog",0,"space=2");                    //  F-view and G-view background color
   zdialog_add_widget(zd,"label","labbg","hbbg",E2X("Background Colors:"),"space=5");
   zdialog_add_widget(zd,"label","space","hbbg",0,"space=5");
   zdialog_add_widget(zd,"label","labfbg","hbbg",E2X("F-View"),"space=5");
   zdialog_add_widget(zd,"colorbutt","FBrgb","hbbg");
   zdialog_add_widget(zd,"label","space","hbbg",0,"space=5");
   zdialog_add_widget(zd,"label","labgbg","hbbg",E2X("G-View"),"space=5");
   zdialog_add_widget(zd,"colorbutt","GBrgb","hbbg");

   zdialog_add_widget(zd,"hbox","hbms","dialog",0,"space=2");                    //  menu style
   zdialog_add_widget(zd,"label","labms","hbms",E2X("Menu Style:"),"space=5");
   zdialog_add_widget(zd,"label","space","hbms",0,"space=5");
   zdialog_add_widget(zd,"radio","icons","hbms",E2X("Icons"),"space=3");
   zdialog_add_widget(zd,"radio","text","hbms",E2X("Text"),"space=3");
   zdialog_add_widget(zd,"radio","both","hbms",E2X("Both"),"space=3");
   zdialog_add_widget(zd,"label","space","hbms",0,"space=10");
   zdialog_add_widget(zd,"label","labis","hbms",E2X("Icon size"));
   zdialog_add_widget(zd,"zspin","iconsize","hbms","26|64|1|32","space=3");      //  20.0
   
   zdialog_add_widget(zd,"hbox","hbmt","dialog",0,"space=2");                    //  menu text and background color
   zdialog_add_widget(zd,"label","labmt","hbmt",E2X("Menu Colors:"),"space=5");
   zdialog_add_widget(zd,"label","space","hbmt",0,"space=5");
   zdialog_add_widget(zd,"label","labmb","hbmt",E2X("Text"),"space=5");
   zdialog_add_widget(zd,"colorbutt","MFrgb","hbmt");
   zdialog_add_widget(zd,"label","space","hbmt",0,"space=5");
   zdialog_add_widget(zd,"label","labmb","hbmt",E2X("Background"),"space=5");
   zdialog_add_widget(zd,"colorbutt","MBrgb","hbmt");

   zdialog_add_widget(zd,"hbox","hbfont","dialog",0,"space=2");                  //  menu and dialog font
   zdialog_add_widget(zd,"label","labfont","hbfont",E2X("Dialog Font:"),"space=5");
   zdialog_add_widget(zd,"label","space","hbfont",0,"space=5");
   zdialog_add_widget(zd,"zentry","font","hbfont","Sans 10","size=20");
   zdialog_add_widget(zd,"button","choosefont","hbfont",Bchoose,"space=5");
   
   zdialog_add_widget(zd,"hbox","hbz","dialog",0,"space=2");                     //  image zoom options
   zdialog_add_widget(zd,"label","labips","hbz","Image Zoom Speed:","space=5");
   zdialog_add_widget(zd,"label","space","hbz",0,"space=5");
   zdialog_add_widget(zd,"zspin","zoomcount","hbz","1|8|1|2","size=3");
   zdialog_add_widget(zd,"label","labz","hbz","zooms per 2x size increase","space=5");

   zdialog_add_widget(zd,"hbox","hbp","dialog",0,"space=2");                     //  image pan options
   zdialog_add_widget(zd,"label","labpan","hbp","Image Pan Mode:","space=5");
   zdialog_add_widget(zd,"label","space","hbp",0,"space=5");
   zdialog_add_widget(zd,"radio","drag","hbp","drag","space=3");
   zdialog_add_widget(zd,"radio","scroll","hbp","scroll","space=3");
   zdialog_add_widget(zd,"check","fast","hbp","fast","space=5");

   zdialog_add_widget(zd,"hbox","hbjpeg","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labqual","hbjpeg",E2X("JPEG file save quality:"),"space=5");
   zdialog_add_widget(zd,"label","space","hbjpeg",0,"space=5");
   zdialog_add_widget(zd,"zspin","jpegqual","hbjpeg","1|100|1|90");
   zdialog_add_widget(zd,"label","labqual","hbjpeg",E2X("(90 = high quality)"),"space=10");

   zdialog_add_widget(zd,"hbox","hbtiff","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labcomp","hbtiff",E2X("TIFF file compression method"),"space=5");
   zdialog_add_widget(zd,"label","space","hbtiff",0,"space=5");
   zdialog_add_widget(zd,"combo","tiffcomp","hbtiff","NONE");
   zdialog_add_widget(zd,"label","labcomp","hbtiff",0,"space=10");

   zdialog_add_widget(zd,"hbox","hbpng","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labcomp","hbpng",E2X("PNG file compression level"),"space=5");
   zdialog_add_widget(zd,"label","space","hbpng",0,"space=5");
   zdialog_add_widget(zd,"zspin","pngcomp","hbpng","1|9|1|1");
   zdialog_add_widget(zd,"label","labcomp","hbpng",0,"space=10");
   
   zdialog_add_widget(zd,"hbox","hbncap","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labncap","hbncap",E2X("Curve Node Separation, Capture Range:"),"space=5");
   zdialog_add_widget(zd,"label","space","hbncap",0,"space=5");
   zdialog_add_widget(zd,"zspin","nodecap","hbncap","3|20|1|5","size=3");
   zdialog_add_widget(zd,"label","labncap","hbncap",pct_scale,"space=10");

   zdialog_add_widget(zd,"hbox","hbmmk","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labmmk","hbmmk",E2X("Map Marker Size:"),"space=5");
   zdialog_add_widget(zd,"label","space","hbmmk",0,"space=5");
   zdialog_add_widget(zd,"zspin","map_dotsize","hbmmk","5|20|1|8","size=3");
   zdialog_add_widget(zd,"label","labmmk","hbmmk","pixels","space=10");

   zdialog_add_widget(zd,"hbox","hbshowver","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labshowver","hbshowver",E2X("Show Images (F-view, G-view):"),"space=5");
   zdialog_add_widget(zd,"label","space","hbshowver",0,"space=5");
   zdialog_add_widget(zd,"radio","lastver","hbshowver",E2X("last version only"),"space=5");
   zdialog_add_widget(zd,"radio","allvers","hbshowver",E2X("all images"),"space=5");

   zdialog_add_widget(zd,"hbox","hbshift","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labshift","hbshift",E2X("Image Position in Window:"),"space=5");
   zdialog_add_widget(zd,"label","space","hbshift",0,"space=5");
   zdialog_add_widget(zd,"radio","centered","hbshift",E2X("centered"),"space=5");
   zdialog_add_widget(zd,"radio","shiftright","hbshift",E2X("right side"),"space=5");

   zdialog_add_widget(zd,"hbox","hbxlev1","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labxlev1","hbxlev1",E2X("Image Index Level:"),"space=5");
   zdialog_add_widget(zd,"hbox","hbxlev2","dialog");
   zdialog_add_widget(zd,"label","space","hbxlev2",0,"space=12");
   zdialog_add_widget(zd,"zspin","indexlev","hbxlev2","0|2|1|2","size=3");
   zdialog_add_widget(zd,"label","labxlev2","hbxlev2",E2X("Fotoxx started directly (2)"),"space=5");
   zdialog_add_widget(zd,"hbox","hbxlev3","dialog");
   zdialog_add_widget(zd,"label","space","hbxlev3",0,"space=12");
   zdialog_add_widget(zd,"zspin","fmindexlev","hbxlev3","0|2|1|1","size=3");
   zdialog_add_widget(zd,"label","labfmxlev2","hbxlev3",E2X("Fotoxx started by file manager (1)"),"space=5");

   zdialog_add_widget(zd,"hbox","hbraw","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labraw","hbraw",E2X("RAW File Loader:"),"space=5");
   zdialog_add_widget(zd,"label","space","hbraw",0,"space=5");
   zdialog_add_widget(zd,"radio","dcraw","hbraw","dcraw","space=3");
   zdialog_add_widget(zd,"radio","rawtherapee","hbraw","RawTherapee","space=3");

   zdialog_add_widget(zd,"hbox","hbrcon1","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labrcon1","hbrcon1",E2X("RAW Conversion Options:"),"space=5");
   zdialog_add_widget(zd,"hbox","hbrcon2","dialog");
   zdialog_add_widget(zd,"label","space","hbrcon2",0,"space=10");
   zdialog_add_widget(zd,"check","autobright","hbrcon2",E2X("extend dynamic range for dim images"),"space=5");
   zdialog_add_widget(zd,"hbox","hbrcon3","dialog");
   zdialog_add_widget(zd,"label","space","hbrcon3",0,"space=10");
   zdialog_add_widget(zd,"check","matchthumb","hbrcon3",E2X("use embedded image as a guide"),"space=5");

   zdialog_add_widget(zd,"hbox","hbrtyp","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labrtyp","hbrtyp",E2X("RAW File Types:"),"space=5");
   zdialog_add_widget(zd,"label","space","hbrtyp",0,"space=5");
   zdialog_add_widget(zd,"zentry","rawtypes","hbrtyp",".raw .rw2");

   zdialog_add_widget(zd,"hbox","hbvtyp","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labvtyp","hbvtyp",E2X("Video File Types:"),"space=5");
   zdialog_add_widget(zd,"label","space","hbvtyp",0,"space=5");
   zdialog_add_widget(zd,"zentry","videotypes","hbvtyp",".mp4 .mov");

   zdialog_add_widget(zd,"hbox","hbvcom","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labvcom","hbvcom",E2X("Video File Play Command:"),"space=5");
   zdialog_add_widget(zd,"label","space","hbvcom",0,"space=5");
   zdialog_add_widget(zd,"comboE","videocomm","hbvcom",video_command,"size=30");

//  stuff dialog fields with current settings
   
   for (ii = 0; ii < 8; ii++)
      zdialog_cb_app(zd,"startopt",startopt[ii][1]);                             //  startup display option list

   for (ii = 0; ii < 8; ii++) {
      if (strmatch(startdisplay,startopt[ii][0]))                                //  set current option
         zdialog_stuff(zd,"startopt",startopt[ii][1]);
   }
   
   snprintf(txrgb,20,"%d|%d|%d",FBrgb[0],FBrgb[1],FBrgb[2]);                     //  F-view background color
   zdialog_stuff(zd,"FBrgb",txrgb);
   snprintf(txrgb,20,"%d|%d|%d",GBrgb[0],GBrgb[1],GBrgb[2]);                     //  G-view background color
   zdialog_stuff(zd,"GBrgb",txrgb);

   zdialog_stuff(zd,"icons",0);                                                  //  menu style
   zdialog_stuff(zd,"text",0);
   zdialog_stuff(zd,"both",0);
   if (strmatch(menu_style,"icons"))
      zdialog_stuff(zd,"icons",1);
   else if (strmatch(menu_style,"text"))
      zdialog_stuff(zd,"text",1);
   else zdialog_stuff(zd,"both",1);

   zdialog_stuff(zd,"iconsize",iconsize);                                        //  icon size

   snprintf(txrgb,20,"%d|%d|%d",MFrgb[0],MFrgb[1],MFrgb[2]);                     //  menus font color
   zdialog_stuff(zd,"MFrgb",txrgb);
   snprintf(txrgb,20,"%d|%d|%d",MBrgb[0],MBrgb[1],MBrgb[2]);                     //  menus background color
   zdialog_stuff(zd,"MBrgb",txrgb);

   zdialog_stuff(zd,"font",dialog_font);                                         //  curr. dialog font

   zdialog_stuff(zd,"zoomcount",zoomcount);                                      //  zooms for 2x increase

   zdialog_stuff(zd,"drag",0);                                                   //  image drag/scroll options
   zdialog_stuff(zd,"scroll",0);
   zdialog_stuff(zd,"fast",0);
   
   if (Fdragopt == 1) zdialog_stuff(zd,"drag",1);                                //  drag image (mouse direction)
   if (Fdragopt == 2) zdialog_stuff(zd,"scroll",1);                              //  scroll image (opposite direction)
   if (Fdragopt == 3) zdialog_stuff(zd,"drag",1);                                //  fast drag
   if (Fdragopt == 4) zdialog_stuff(zd,"scroll",1);                              //  fast scroll
   if (Fdragopt >= 3) zdialog_stuff(zd,"fast",1);                                //  fast option

   zdialog_stuff(zd,"jpegqual",jpeg_def_quality);                                //  default jpeg file save quality

   for (ii = 0; ii < NTO; ii++)                                                  //  TIFF file compression options      20.0
      zdialog_cb_app(zd,"tiffcomp",tiffopt[ii][0]);
   
   for (ii = 0; ii < NTO; ii++)                                                  //  set current option                 20.0
      if (tiff_comp_method == atoi(tiffopt[ii][1]))
         zdialog_stuff(zd,"tiffcomp",tiffopt[ii][0]); 
   
   zdialog_stuff(zd,"pngcomp",png_comp_level);                                   //  current PNG compression level      20.0

   zdialog_stuff(zd,"nodecap",splcurve_minx);                                    //  edit curve min. node distance 

   zdialog_stuff(zd,"map_dotsize",map_dotsize);                                  //  map dot size
   
   if (Flastversion) {
      zdialog_stuff(zd,"lastver",1);                                             //  prev/next shows last version only
      zdialog_stuff(zd,"allvers",0);
   }
   else {
      zdialog_stuff(zd,"lastver",0);                                             //  prev/next shows all versions
      zdialog_stuff(zd,"allvers",1);
   }

   if (Fshiftright) {
      zdialog_stuff(zd,"shiftright",1);                                          //  shift image to window right side
      zdialog_stuff(zd,"centered",0);
   }
   else {
      zdialog_stuff(zd,"shiftright",0);                                          //  keep image centered in window
      zdialog_stuff(zd,"centered",1);
   }

   zdialog_stuff(zd,"indexlev",Findexlev);                                       //  index level, always
   zdialog_stuff(zd,"fmindexlev",FMindexlev);                                    //  index level, file manager call

   if (Frawloader == 1) {                                                        //  RAW file loader option             20.0
      zdialog_stuff(zd,"dcraw",1);
      zdialog_stuff(zd,"rawtherapee",0);
   }
   else {
      zdialog_stuff(zd,"dcraw",0);
      zdialog_stuff(zd,"rawtherapee",1);
   }
   
   zdialog_stuff(zd,"autobright",Fautobright);                                   //  RAW loader, auto-brighten option   20.0
   zdialog_stuff(zd,"matchthumb",Fmatchthumb);                                   //   " "  match embedded image color   20.0

   zdialog_stuff(zd,"rawtypes",RAWfiletypes);                                    //  RAW file types

   zdialog_stuff(zd,"videotypes",VIDEOfiletypes);                                //  VIDEO file types

   zdialog_cb_app(zd,"videocomm",video_command);                                 //  video player options               20.0
   zdialog_cb_app(zd,"videocomm","gst-launch-1.0 playbin uri=file://");
   zdialog_cb_app(zd,"videocomm","ffplay -loglevel 8 -autoexit ");
   zdialog_cb_app(zd,"videocomm","totem ");
   zdialog_cb_app(zd,"videocomm","vlc ");
   zdialog_cb_app(zd,"videocomm","firefox ");
   zdialog_cb_app(zd,"videocomm","chromium-browser ");
   
//  run dialog and wait for completion

   zdialog_resize(zd,500,600);
   zdialog_run(zd,preferences_dialog_event,"parent");                            //  run dialog and wait for completion
   zstat = zdialog_wait(zd);
   zdialog_free(zd);

   Fblock = 0;

   if (zstat == 1) {
      new_session(0);                                                            //  start new session if changes made
      zsleep(1);                                                                 //  delay before SIGTERM in quitxx()   19.0
      quitxx();
   }

   return;
}


//  preferences dialog event function

int preferences_dialog_event(zdialog *zd, cchar *event)
{
   using namespace preferences;

   int            ii, jj, nn;
   char           *pp, temp[200];
   cchar          *ppc;
   char           txrgb[20];
   GtkWidget      *font_dialog;
   
   if (zd->zstat && zd->zstat != 1) return 1;                                    //  cancel

   if (strmatch(event,"browse"))                                                 //  browse for startup folder or file
   {
      zdialog_fetch(zd,"startopt",temp,200);                                     //  get startup option
      for (ii = 0; ii < 8; ii++) {
         if (strmatch(temp,startopt[ii][1])) {
            zfree(startdisplay);
            startdisplay = zstrdup(startopt[ii][0]);
         }
      }
      
      if (! choosefile && topfolders[0])                                         //  set a default location
         choosefile = zstrdup(topfolders[0]);

      if (strmatch(startdisplay,"specG")) {                                      //  specific gallery
         pp = zgetfile(E2X("Select startup folder"),MWIN,"folder",choosefile);
         if (! pp) return 1;
         zfree(choosefile);
         choosefile = pp;
      }

      if (strmatch(startdisplay,"specF")) {                                      //  specific image file
         pp = zgetfile(E2X("Select startup image file"),MWIN,"file",choosefile);
         if (! pp) return 1;
         zfree(choosefile);
         choosefile = pp;
      }
      
      if (strmatch(startdisplay,"album")) {                                      //  specific album
         pp = zgetfile(E2X("Select startup album"),MWIN,"file",albums_folder);
         if (! pp) return 1;
         zfree(choosefile);
         choosefile = pp;
      }
   }

   if (strmatch(event,"choosefont"))                                             //  choose menu/dialog font
   {
      zdialog_fetch(zd,"font",temp,200);
      font_dialog = gtk_font_chooser_dialog_new(E2X("select font"),MWIN);
      gtk_font_chooser_set_font(GTK_FONT_CHOOSER(font_dialog),temp);
      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) return 1;
      zdialog_stuff(zd,"font",pp);
      zsetfont(pp);
      dialog_font = zstrdup(pp);
      g_free(pp);
   }

   if (strmatch(event,"rawtherapee"))
   {   
      if (! Frawtherapeecli) {
         zmessageACK(Mwin,E2X("rawtherapee-cli (rawtherapee) is not installed"));
         zdialog_stuff(zd,"dcraw",1);
         zdialog_stuff(zd,"rawtherapee",0);
         Frawloader = 1;
      }
   }

//  wait for dialog completion, process all inputs

   if (zd->zstat != 1) return 1;                                                 //  wait for [done] button

   if (choosefile)                                                               //  startup file/gallery/album
   {
      zdialog_fetch(zd,"startopt",temp,200);                                     //  get startup option
      for (ii = 0; ii < 8; ii++) {
         if (strmatch(temp,startopt[ii][1])) {
            zfree(startdisplay);
            startdisplay = zstrdup(startopt[ii][0]);
         }
      }
      
      if (strmatch(startdisplay,"album")) {                                      //  startup option = album 
         if (startalbum) zfree(startalbum);
         startalbum = zstrdup(choosefile);
      }

      if (strmatch(startdisplay,"specG")) {                                      //  startup option = folder gallery
         if (startfolder) zfree(startfolder);
         startfolder = zstrdup(choosefile);
         if (image_file_type(startfolder) != FDIR) {
            zmessageACK(Mwin,E2X("startup folder is invalid"));
            zd->zstat = 0;
            return 1;
         }
      }

      if (strmatch(startdisplay,"specF")) {                                      //  startup option = image file
         if (startfile) zfree(startfile);
         startfile = zstrdup(choosefile);
         if (image_file_type(startfile) != IMAGE) {
            zmessageACK(Mwin,E2X("startup file is invalid"));
            zd->zstat = 0;
            return 1;
         }
      }
   }

   zdialog_fetch(zd,"FBrgb",txrgb,20);                                           //  F-view background color
   ppc = strField(txrgb,"|",1);
   if (ppc) FBrgb[0] = atoi(ppc);
   ppc = strField(txrgb,"|",2);
   if (ppc) FBrgb[1] = atoi(ppc);
   ppc = strField(txrgb,"|",3);
   if (ppc) FBrgb[2] = atoi(ppc);

   zdialog_fetch(zd,"GBrgb",txrgb,20);                                           //  G-view background color
   ppc = strField(txrgb,"|",1);
   if (ppc) GBrgb[0] = atoi(ppc);
   ppc = strField(txrgb,"|",2);
   if (ppc) GBrgb[1] = atoi(ppc);
   ppc = strField(txrgb,"|",3);
   if (ppc) GBrgb[2] = atoi(ppc);

   zdialog_fetch(zd,"icons",nn);                                                 //  menu style = icons
   if (nn) {
      if (! strmatch(menu_style,"icons")) {
         zfree(menu_style);
         menu_style = zstrdup("icons");
      }
   }

   zdialog_fetch(zd,"text",nn);                                                  //  menu style = text
   if (nn) {
      if (! strmatch(menu_style,"text")) {
         zfree(menu_style);
         menu_style = zstrdup("text");
      }
   }

   zdialog_fetch(zd,"both",nn);                                                  //  menu style = icons + text
   if (nn) {
      if (! strmatch(menu_style,"both")) {
         zfree(menu_style);
         menu_style = zstrdup("both");
      }
   }
   
   zdialog_fetch(zd,"iconsize",nn);                                              //  icon size
   if (nn != iconsize) {
      iconsize = nn;
   }
   
   zdialog_fetch(zd,"MFrgb",txrgb,20);                                           //  menu text color
   ppc = strField(txrgb,"|",1);
   if (ppc) MFrgb[0] = atoi(ppc);
   ppc = strField(txrgb,"|",2);
   if (ppc) MFrgb[1] = atoi(ppc);
   ppc = strField(txrgb,"|",3);
   if (ppc) MFrgb[2] = atoi(ppc);

   zdialog_fetch(zd,"MBrgb",txrgb,20);                                           //  menu background color
   ppc = strField(txrgb,"|",1);
   if (ppc) MBrgb[0] = atoi(ppc);
   ppc = strField(txrgb,"|",2);
   if (ppc) MBrgb[1] = atoi(ppc);
   ppc = strField(txrgb,"|",3);
   if (ppc) MBrgb[2] = atoi(ppc);

   zdialog_fetch(zd,"zoomcount",zoomcount);                                      //  zooms for 2x image size
   zoomratio = pow( 2.0, 1.0 / zoomcount);                                       //  2.0, 1.4142, 1.2599, 1.1892 ...

   zdialog_fetch(zd,"drag",nn);                                                  //  drag/scroll option
   if (nn) Fdragopt = 1;                                                         //  1/2 = drag/scroll
   else Fdragopt = 2;
   zdialog_fetch(zd,"fast",nn);                                                  //  3/4 = drag/scroll fast
   if (nn) Fdragopt += 2;

   zdialog_fetch(zd,"jpegqual",jpeg_def_quality);                                //  JPEG file save quality
   
   zdialog_fetch(zd,"tiffcomp",temp,20);                                         //  TIFF file compression method
   for (ii = 0; ii < NTO; ii++)
      if (strmatch(temp,tiffopt[ii][0])) break;
   if (ii < NTO) tiff_comp_method = atoi(tiffopt[ii][1]);
   
   zdialog_fetch(zd,"pngcomp",png_comp_level);                                   //  PNG file compression level

   zdialog_fetch(zd,"nodecap",splcurve_minx);                                    //  edit curve min. node distance
   zdialog_fetch(zd,"map_dotsize",map_dotsize);                                  //  map dot size
   zdialog_fetch(zd,"lastver",Flastversion);                                     //  prev/next shows last version only
   zdialog_fetch(zd,"shiftright",Fshiftright);                                   //  shift image to right margin
   zdialog_fetch(zd,"indexlev",Findexlev);                                       //  index level, started directly
   zdialog_fetch(zd,"fmindexlev",FMindexlev);                                    //   ... started via file manager

   zdialog_fetch(zd,"dcraw",nn);                                                 //  RAW loader selection               20.0
   if (nn == 1) Frawloader = 1;
   else Frawloader = 2;

   zdialog_fetch(zd,"autobright",Fautobright);                                   //  RAW loader, auto-brighten option   20.0
   zdialog_fetch(zd,"matchthumb",Fmatchthumb);                                   //   " "  match embedded image color   20.0

   zdialog_fetch(zd,"rawtypes",temp,200);                                        //  RAW file types, .raw .rw2 ...
   pp = zstrdup(temp,100);

   for (ii = jj = 0; temp[ii]; ii++) {                                           //  insure blanks between types
      if (temp[ii] == '.' && temp[ii-1] != ' ') pp[jj++] = ' ';
      pp[jj++] = temp[ii];
   }
   if (pp[jj-1] != ' ') pp[jj++] = ' ';                                          //  insure 1 final blank
   pp[jj] = 0;

   if (RAWfiletypes) zfree(RAWfiletypes);
   RAWfiletypes = pp;

   zdialog_fetch(zd,"videotypes",temp,200);                                      //  VIDEO file types, .mp4 .mov ...
   pp = zstrdup(temp,100);

   for (ii = jj = 0; temp[ii]; ii++) {                                           //  insure blanks between types
      if (temp[ii] == '.' && temp[ii-1] != ' ') pp[jj++] = ' ';
      pp[jj++] = temp[ii];
   }
   if (pp[jj-1] != ' ') pp[jj++] = ' ';                                          //  insure 1 final blank
   pp[jj] = 0;

   if (VIDEOfiletypes) zfree(VIDEOfiletypes);
   VIDEOfiletypes = pp;

   zdialog_fetch(zd,"videocomm",temp,200);                                       //  selected video command             20.0
   zfree(video_command);
   video_command = zstrdup(temp);

   save_params();                                                                //  done, save modified parameters
   Fpaint2();
   return 1;
}


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

//  keyboard shortcuts

namespace KBshortcutnames
{
   zdialog     *zd;

   int         Nreserved = 14;                                                   //  reserved shortcuts (hard coded)
   cchar       *reserved[14] = { 
      "F1", "F10", "F11", "Escape", "Delete", "Left", "Right", 
      "Up", "Down", "First", "Last", "Page_Up", "Page_Down", "Z" };

   kbsutab_t   kbsutab2[maxkbsu];                                                //  KB shortcuts list during editing
   int         Nkbsu2;
}


//  KB shortcuts menu function

void m_KBshortcuts(GtkWidget *, cchar *)
{
   using namespace KBshortcutnames;
   
   int  KBshorts_dialog_event(zdialog *zd, cchar *event);
   void KBshortcuts_edit();

   int         zstat, ii;
   GtkWidget   *widget;

   F1_help_topic = "KB shortcuts";

/***
          ____________________________________________
         |          Keyboard Shortcuts                |
         |                                            |
         | Reserved Shortcuts                         |
         |  Z              Toggle 1x / fit window     |
         |  F1             User Guide, Context Help   |
         |  F10            Full Screen with menus     |
         |  F11            Full Screen without menus  |
         |  Escape         Quit dialog, Quit Fotoxx   |
         |  Delete         Delete/Trash               |
         |  Arrow keys     Navigation                 |
         |  Page keys      Navigation                 |
         |  Home/End       Navigation                 |
         |                                            |
         | Custom Shortcuts                           |
         |  +              Zoom+                      |
         |  -              Zoom-                      |
         |  F              File View                  |
         |  G              Gallery View               |
         |  M              Map View                   |
         |  L              List View                  |
         |  R              Rename                     |
         |  K              KB Shortcuts               |
         |  T              Trim/Rotate                |
         | ...             ...                        |
         |                                            |
         |                            [edit] [cancel] |
         |____________________________________________|
         
***/

   zd = zdialog_new(E2X("Keyboard Shortcuts"),Mwin,Bedit,Bcancel,null);
   zdialog_add_widget(zd,"scrwin","scrlist","dialog",0,"space=5|expand");
   zdialog_add_widget(zd,"text","shortlist","scrlist",0,"expand");

   widget = zdialog_widget(zd,"shortlist");                                      //  list fixed shortcuts

   textwidget_append(widget,1,E2X("Reserved Shortcuts \n"));
   textwidget_append(widget,0,E2X(" Z           Toggle 1x / fit window \n"));
   textwidget_append(widget,0,E2X(" F1          User Guide, Context Help \n"));
   textwidget_append(widget,0,E2X(" F10         Full Screen with menus \n"));
   textwidget_append(widget,0,E2X(" F11         Full Screen without menus \n"));
   textwidget_append(widget,0,E2X(" Escape      Quit dialog, Quit Fotoxx \n"));
   textwidget_append(widget,0,E2X(" Delete      Delete/Trash \n"));
   textwidget_append(widget,0,E2X(" Arrow keys  Navigation \n"));
   textwidget_append(widget,0,E2X(" Page keys   Navigation \n"));
   textwidget_append(widget,0,E2X(" Home/End    Navigation \n"));

   textwidget_append(widget,0,"\n");
   textwidget_append(widget,1,"Custom Shortcuts \n");

   for (ii = 0; ii < Nkbsu; ii++)                                                //  list custom shortcuts 
      textwidget_append(widget,0," %-14s %s \n",
                     kbsutab[ii].key, E2X(kbsutab[ii].menu));
   
   zdialog_resize(zd,400,600);
   zdialog_run(zd,KBshorts_dialog_event,"save");
   zstat = zdialog_wait(zd);
   zdialog_free(zd);
   if (zstat == 1) KBshortcuts_edit();
   return;
}


//  dialog event and completion function

int KBshorts_dialog_event(zdialog *zd, cchar *event)
{
   if (zd->zstat) zdialog_destroy(zd);
   return 1;
}


//  KB shortcuts edit function 

void KBshortcuts_edit()
{
   using namespace KBshortcutnames;

   void KBshorts_callbackfunc1(GtkWidget *widget, int line, int pos, int kbkey);
   void KBshorts_callbackfunc2(GtkWidget *widget, int line, int pos, int kbkey);
   int  KBshorts_keyfunc(GtkWidget *dialog, GdkEventKey *event);
   int  KBshorts_edit_dialog_event(zdialog *zd, cchar *event);

   int         ii;
   GtkWidget   *widget;
   char        *sortlist[maxkbsf];

/***
          _________________________________________________________________
         |              Edit KB Shortcuts                                  |
         |_________________________________________________________________|
         | Alt+G           Grid Lines              |  Blur                 |
         | Alt+U           Undo                    |  Bookmarks            |
         | Alt+R           Redo                    |  Captions             |
         | T               Trim/Rotate             |  Color Depth          |
         | V               View Main               |  Color Sat            |
         | Ctrl+Shift+V    View All                |  Copy to Cache        |
         | ...             ...                     |  Copy to Clipboard    |
         | Done            dialog button           |  Current Album        |     //  dialog button KB shortcut
         | ...             ...                     |  ...                  |
         |_________________________________________|_______________________|
         |                                                                 |
         | shortcut key: (enter key)  (no selection)                       |
         |                                                                 |
         |                                  [add] [remove] [done] [cancel] |
         |_________________________________________________________________|

***/

   zd = zdialog_new(E2X("Edit KB Shortcuts"),Mwin,Badd,Bdelete,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hblists","dialog",0,"expand");
   zdialog_add_widget(zd,"scrwin","scrlist","hblists",0,"space=5|expand");
   zdialog_add_widget(zd,"text","shortlist","scrlist",0,"expand");
   zdialog_add_widget(zd,"scrwin","scrmenus","hblists",0,"expand");
   zdialog_add_widget(zd,"text","menufuncs","scrmenus");
   zdialog_add_widget(zd,"hbox","hbshort","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labshort","hbshort",E2X("shortcut key:"),"space=5");
   zdialog_add_widget(zd,"label","shortkey","hbshort",E2X("(enter key)"),"size=10");
   zdialog_add_widget(zd,"label","shortfunc","hbshort",E2X("(no selection)"),"space=5");
   
   for (ii = 0; ii < Nkbsu; ii++) {                                              //  copy current shortcuts list
      kbsutab2[ii].key = zstrdup(kbsutab[ii].key);
      kbsutab2[ii].menu = zstrdup(kbsutab[ii].menu);
   }
   Nkbsu2 = Nkbsu;

   widget = zdialog_widget(zd,"shortlist");                                      //  show shortcuts list in dialog,
   textwidget_clear(widget);                                                     //    translated
   for (ii = 0; ii < Nkbsu2; ii++)
      textwidget_append(widget,0,"%-14s %s \n",
                           kbsutab2[ii].key, E2X(kbsutab2[ii].menu));

   textwidget_set_eventfunc(widget,KBshorts_callbackfunc1);                      //  set mouse/KB event function

   for (ii = 0; ii < Nkbsf; ii++)                                                //  copy eligible shortcut funcs,
      sortlist[ii] = zstrdup(E2X(kbsftab[ii].menu));                             //    translated

   HeapSort(sortlist,Nkbsf);                                                     //  sort copied list

   widget = zdialog_widget(zd,"menufuncs");                                      //  clear dialog
   textwidget_clear(widget);

   for (ii = 0; ii < Nkbsf; ii++)                                                //  show sorted translated funcs list
      textwidget_append(widget,0,"%s\n",sortlist[ii]);

   textwidget_set_eventfunc(widget,KBshorts_callbackfunc2);                      //  set mouse/KB event function

   widget = zdialog_widget(zd,"dialog");                                         //  capture KB keys pressed
   G_SIGNAL(widget,"key-press-event",KBshorts_keyfunc,0);

   zdialog_resize(zd,600,400);
   zdialog_run(zd,KBshorts_edit_dialog_event,"save");

   return;
}


//  mouse callback function to select existing shortcut from list
//    and stuff into dialog "shortfunc"

void KBshorts_callbackfunc1(GtkWidget *widget, int line, int pos, int kbkey)
{
   using namespace KBshortcutnames;

   char     *txline;
   char     shortkey[20];
   char     shortfunc[60];

   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }

   txline = textwidget_line(widget,line,1);                                      //  get clicked line
   if (! txline || ! *txline) return;
   textwidget_highlight_line(widget,line);

   strncpy0(shortkey,txline,14);                                                 //  get shortcut key and menu
   strncpy0(shortfunc,txline+15,60);
   zfree(txline);

   strTrim(shortkey);
   strTrim(shortfunc);

   zdialog_stuff(zd,"shortkey",shortkey);                                        //  stuff into dialog
   zdialog_stuff(zd,"shortfunc",shortfunc);

   return;
}


//  mouse callback function to select new shortcut function from menu list
//    and stuff into dialog "shortfunc"

void KBshorts_callbackfunc2(GtkWidget *widget, int line, int pos, int kbkey)
{
   using namespace KBshortcutnames;
   
   char     *txline;
   
   if (kbkey == GDK_KEY_F1) {                                                    //  key F1 pressed, show help
      showz_docfile(Mwin,"userguide",F1_help_topic);
      return;
   }
   
   txline = textwidget_line(widget,line,1);                                      //  get clicked line
   if (! txline || ! *txline) return;
   textwidget_highlight_line(widget,line);

   zdialog_stuff(zd,"shortfunc",txline);                                         //  stuff into dialog
   zfree(txline);
   return;
}


//  intercept KB key events, stuff into dialog "shortkey"

int KBshorts_keyfunc(GtkWidget *dialog, GdkEventKey *event)
{
   using namespace KBshortcutnames;

   int      Ctrl = 0, Alt = 0, Shift = 0;
   int      key, ii, cc;
   char     keyname[20];

   key = event->keyval;

   if (event->state & GDK_CONTROL_MASK) Ctrl = 1;
   if (event->state & GDK_SHIFT_MASK) Shift = 1;
   if (event->state & GDK_MOD1_MASK) Alt = 1;
   
   if (key == GDK_KEY_Escape) return 0;                                          //  pass escape (cancel) to zdialog
   
   if (key == GDK_KEY_F1) {                                                      //  key is F1 (context help)
      KBevent(event);                                                            //  send to main app
      return 1;
   }

   if (key >= GDK_KEY_F2 && key <= GDK_KEY_F9) {                                 //  key is F2 to F9
      ii = key - GDK_KEY_F1;
      strcpy(keyname,"F1");
      keyname[1] += ii;
   }

   else if (key > 255) return 1;                                                 //  not a simple Ascii key

   else {
      *keyname = 0;                                                              //  build input key combination
      if (Ctrl) strcat(keyname,"Ctrl+");                                         //  [Ctrl+] [Alt+] [Shift+] key
      if (Alt) strcat(keyname,"Alt+");
      if (Shift) strcat(keyname,"Shift+");
      cc = strlen(keyname);
      keyname[cc] = toupper(key);                                                //  x --> X, Ctrl+x --> Ctrl+X
      keyname[cc+1] = 0;
   }

   for (ii = 0; ii < Nreserved; ii++)
      if (strmatch(keyname,reserved[ii])) break;
   if (ii < Nreserved) {
      zmessageACK(Mwin,E2X("\"%s\"  Reserved, cannot be used"),keyname);
      Ctrl = Alt = 0;
      return 1;
   }

   zdialog_stuff(zd,"shortkey",keyname);                                         //  stuff key name into dialog
   zdialog_stuff(zd,"shortfunc",E2X("(no selection)"));                          //  clear menu choice

   return 1;
}


//  dialog event and completion function

int KBshorts_edit_dialog_event(zdialog *zd, cchar *event)
{
   using namespace KBshortcutnames;

   int  KBshorts_edit_menufuncs_event(zdialog *zd, cchar *event);
   void KBshorts_callbackfunc2(GtkWidget *widget, int line, int pos, int kbkey);
   
   int         ii, jj;
   GtkWidget   *widget;
   char        shortkey[20];
   char        shortfunc[60];
   char        kbfile[200];
   FILE        *fid = 0;

   if (! zd->zstat) return 1;                                                    //  wait for completion
   
   if (zd->zstat == 1)                                                           //  add shortcut
   {
      zd->zstat = 0;                                                             //  keep dialog active

      if (Nkbsu2 == maxkbsu) {
         zmessageACK(Mwin,"exceed %d shortcuts",maxkbsu);
         return 1;
      }

      zdialog_fetch(zd,"shortkey",shortkey,20);                                  //  get shortcut key and menu
      zdialog_fetch(zd,"shortfunc",shortfunc,60);                                //    from dialog widgets
      if (*shortkey <= ' ' || *shortfunc <= ' ') return 0;

      for (ii = 0; ii < Nkbsu2; ii++)                                            //  find matching shortcut key in list
         if (strmatch(kbsutab2[ii].key,shortkey)) break;

      if (ii < Nkbsu2) {                                                         //  if found, remove from list
         zfree(kbsutab2[ii].key);
         zfree(kbsutab2[ii].menu);
         for (jj = ii; jj < Nkbsu2; jj++)
            kbsutab2[jj] = kbsutab2[jj+1];
         --Nkbsu2;
      }

      for (ii = 0; ii < Nkbsu2; ii++)                                            //  find matching shortcut func in list
         if (strmatch(shortfunc,E2X(kbsutab2[ii].menu))) break;

      if (ii < Nkbsu2) {                                                         //  if found, remove from list
         zfree(kbsutab2[ii].key);
         zfree(kbsutab2[ii].menu);
         for (jj = ii; jj < Nkbsu2; jj++)
            kbsutab2[jj] = kbsutab2[jj+1];
         --Nkbsu2;
      }
      
      for (ii = 0; ii < Nkbsf; ii++)                                             //  look up shortcut func in list
         if (strmatch(shortfunc,E2X(kbsftab[ii].menu))) break;                   //    of eligible menu funcs
      if (ii == Nkbsf) return 1;
      strncpy0(shortfunc,kbsftab[ii].menu,60);                                   //  english menu func

      ii = Nkbsu2++;                                                             //  add new shortcut to end of list
      kbsutab2[ii].key = zstrdup(shortkey);
      kbsutab2[ii].menu = zstrdup(shortfunc);

      widget = zdialog_widget(zd,"shortlist");                                   //  clear shortcuts list
      textwidget_clear(widget);

      for (ii = 0; ii < Nkbsu2; ii++)                                            //  show updated shortcuts in dialog
         textwidget_append2(widget,0,"%-14s %s \n",
                              kbsutab2[ii].key,E2X(kbsutab2[ii].menu));
      return 1;
   }
   
   if (zd->zstat == 2)                                                           //  remove shortcut
   {
      zd->zstat = 0;                                                             //  keep dialog active

      zdialog_fetch(zd,"shortkey",shortkey,20);                                  //  get shortcut key
      if (*shortkey <= ' ') return 0;

      for (ii = 0; ii < Nkbsu2; ii++)                                            //  find matching shortcut key in list
         if (strmatch(kbsutab2[ii].key,shortkey)) break;

      if (ii < Nkbsu2) {                                                         //  if found, remove from list
         zfree(kbsutab2[ii].key);
         zfree(kbsutab2[ii].menu);
         for (jj = ii; jj < Nkbsu2; jj++)
            kbsutab2[jj] = kbsutab2[jj+1];
         --Nkbsu2;
      }

      widget = zdialog_widget(zd,"shortlist");                                   //  clear shortcuts list
      textwidget_clear(widget);

      for (ii = 0; ii < Nkbsu2; ii++)                                            //  show updated shortcuts in dialog
         textwidget_append2(widget,0,"%-14s %s \n",
                              kbsutab2[ii].key,E2X(kbsutab2[ii].menu));

      zdialog_stuff(zd,"shortkey","");                                           //  clear entered key and menu
      zdialog_stuff(zd,"shortfunc",E2X("(no selection)"));
      return 1;
   }

   if (zd->zstat == 3)                                                           //  done - save new shortcut list
   {
      zdialog_free(zd);                                                          //  kill menu funcs list

      locale_filespec("user","KB-shortcuts20",kbfile);                           //  update shortcuts file              20.0
      fid = fopen(kbfile,"w");
      if (! fid) {
         zmessageACK(Mwin,strerror(errno));
         return 1;
      }
      for (ii = 0; ii < Nkbsu2; ii++)
         fprintf(fid,"%-14s %s \n",kbsutab2[ii].key,kbsutab2[ii].menu);
      fclose(fid);

      KBshortcuts_load();                                                        //  reload shortcuts list from file
      return 1;
   }

   zdialog_free(zd);                                                             //  cancel or [x]
   return 1;
}


//  read KB-shortcuts file and load shortcuts table in memory
//  also called from initzfunc() at fotoxx startup

int KBshortcuts_load()
{
   int         ii, err;
   char        kbfile[200], buff[200];
   char        *pp1, *pp2;
   char        *key, *menu;
   FILE        *fid;

   for (ii = 0; ii < Nkbsu; ii++) {                                              //  clear shortcuts data 
      zfree(kbsutab[ii].key);
      zfree(kbsutab[ii].menu);
   }
   Nkbsu = 0;

   err = locale_filespec("user","KB-shortcuts20",kbfile);                        //  20.0
   if (err) return 0;
   fid = fopen(kbfile,"r");
   if (! fid) return 0;
   
   for (ii = 0; ii < maxkbsu; )
   {
      pp1 = fgets_trim(buff,200,fid,1);                                          //  next record
      if (! pp1) break;
      if (*pp1 == '#') continue;                                                 //  comment
      if (*pp1 <= ' ') continue;                                                 //  blank
      pp2 = strchr(pp1,' ');
      if (! pp2) continue;
      if (pp2 - pp1 > 20) continue;
      *pp2 = 0;
      key = zstrdup(pp1);                                                        //  shortcut key or combination
      pp1 = pp2 + 1;
      while (*pp1 && *pp1 == ' ') pp1++;
      if (! *pp1) continue;
      menu = zstrdup(pp1);                                                       //  corresp. menu, English

      if (strstr(zdialog_button_shortcuts,menu))                                 //  if a dialog button shortcut
         zdialog_KB_addshortcut(key,menu);                                       //    register with zdialog

      kbsutab[ii].key = zstrdup(key);                                            //  add to shortcuts table
      kbsutab[ii].menu = zstrdup(menu);
      ii++;
   }

   Nkbsu = ii;

   fclose(fid);
   return 0;
}


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

//  show a brightness distribution graph - live update as image is edited

namespace RGB_dist_names
{
   GtkWidget   *drawwin_dist, *drawwin_scale;                                    //  brightness distribution graph widgets
   int         RGBW[4] = { 1, 1, 1, 0 };                                         //     "  colors: red/green/blue/white (all)
}


//  menu function

void m_RGB_dist(GtkWidget *, cchar *menu)                                        //  menu function
{
   using namespace RGB_dist_names;

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

   GtkWidget   *frdist, *frscale, *widget;
   zdialog     *zd;

   m_viewmode(0,"F");                                                            //  file view mode                     19.0
   if (! curr_file) return;

   if (menu && strmatch(menu,"kill")) {
      if (zd_RGB_dist) zdialog_free(zd_RGB_dist);
      zd_RGB_dist = 0;
      return;
   }

   if (zd_RGB_dist) {                                                            //  dialog already present
      gtk_widget_queue_draw(drawwin_dist);                                       //  refresh drawing windows
      return;
   }

   if (menu) F1_help_topic = "RGB distribution";

   zd = zdialog_new(E2X("Brightness Distribution"),Mwin,null);
   zdialog_add_widget(zd,"frame","frdist","dialog",0,"expand");                  //  frames for 2 drawing areas
   zdialog_add_widget(zd,"frame","frscale","dialog");
   frdist = zdialog_widget(zd,"frdist");
   frscale = zdialog_widget(zd,"frscale");

   drawwin_dist = gtk_drawing_area_new();                                        //  distribution drawing area
   gtk_container_add(GTK_CONTAINER(frdist),drawwin_dist);
   G_SIGNAL(drawwin_dist,"draw",RGB_dist_graph,RGBW);

   drawwin_scale = gtk_drawing_area_new();                                       //  brightness scale under distribution
   gtk_container_add(GTK_CONTAINER(frscale),drawwin_scale);
   gtk_widget_set_size_request(drawwin_scale,300,12);
   G_SIGNAL(drawwin_scale,"draw",brightness_scale,0);

   zdialog_add_widget(zd,"hbox","hbcolors","dialog");
   zdialog_add_widget(zd,"check","all","hbcolors",Ball,"space=5");
   zdialog_add_widget(zd,"check","red","hbcolors",Bred,"space=5");
   zdialog_add_widget(zd,"check","green","hbcolors",Bgreen,"space=5");
   zdialog_add_widget(zd,"check","blue","hbcolors",Bblue,"space=5");

   zdialog_stuff(zd,"red",RGBW[0]);
   zdialog_stuff(zd,"green",RGBW[1]);
   zdialog_stuff(zd,"blue",RGBW[2]);
   zdialog_stuff(zd,"all",RGBW[3]);

   zdialog_resize(zd,300,250);
   zdialog_run(zd,show_RGB_dist_dialog_event,"save");

   widget = zdialog_widget(zd,"dialog");                                         //  stop focus on this window
   gtk_window_set_accept_focus(GTK_WINDOW(widget),0);

   zd_RGB_dist = zd;
   return;
}


//  dialog event and completion function

int show_RGB_dist_dialog_event(zdialog *zd, cchar *event)
{
   using namespace RGB_dist_names;

   if (zd->zstat) {
      zdialog_free(zd);
      zd_RGB_dist = 0;
      return 1;
   }

   if (zstrstr("all red green blue",event)) {                                    //  update chosen colors
      zdialog_fetch(zd,"red",RGBW[0]);
      zdialog_fetch(zd,"green",RGBW[1]);
      zdialog_fetch(zd,"blue",RGBW[2]);
      zdialog_fetch(zd,"all",RGBW[3]);
      gtk_widget_queue_draw(drawwin_dist);                                       //  refresh drawing window
   }

   return 1;
}


//  draw brightness distribution graph in drawing window 

void RGB_dist_graph(GtkWidget *drawin, cairo_t *cr, int *rgbw)
{
   int         bin, Nbins = 256, distribution[256][4];                           //  bin count, R/G/B/all
   int         px, py, dx, dy;
   int         ww, hh, winww, winhh;
   int         ii, rgb, maxdist, bright;
   uint8       *pixi;

   if (! Fpxb) return;
   if (rgbw[0]+rgbw[1]+rgbw[2]+rgbw[3] == 0) return;

   winww = gtk_widget_get_allocated_width(drawin);                               //  drawing window size
   winhh = gtk_widget_get_allocated_height(drawin);

   for (bin = 0; bin < Nbins; bin++)                                             //  clear brightness distribution
   for (rgb = 0; rgb < 4; rgb++)
      distribution[bin][rgb] = 0;

   ww = Fpxb->ww;                                                                //  image area within window
   hh = Fpxb->hh;

   for (ii = 0; ii < ww * hh; ii++)
   {
      if (sa_stat == 3 && ! sa_pixmap[ii]) continue;                             //  stay within active select area

      py = ii / ww;                                                              //  image pixel
      px = ii - ww * py;

      dx = Mscale * px - Morgx + Dorgx;                                          //  stay within visible window
      if (dx < 0 || dx > Dww-1) continue;                                        //    for zoomed image
      dy = Mscale * py - Morgy + Dorgy;
      if (dy < 0 || dy > Dhh-1) continue;

      pixi = PXBpix(Fpxb,px,py);                                                 //  use displayed image

      for (rgb = 0; rgb < 3; rgb++) {                                            //  get R/G/B brightness levels
         bright = pixi[rgb] * Nbins / 256.0;                                     //  scale 0 to Nbins-1
         if (bright < 0 || bright > 255) {
            printz("pixel %d/%d: %d %d %d \n",px,py,pixi[0],pixi[1],pixi[2]);
            return;
         }
         ++distribution[bright][rgb];
      }

      bright = (pixi[0] + pixi[1] + pixi[2]) * 0.333 * Nbins / 256.0;            //  R+G+B, 0 to Nbins-1
      ++distribution[bright][3];
   }

   maxdist = 0;

   for (bin = 1; bin < Nbins-1; bin++)                                           //  find max. bin over all RGB
   for (rgb = 0; rgb < 3; rgb++)                                                 //    omit bins 0 and last
      if (distribution[bin][rgb] > maxdist)                                      //      which can be huge
         maxdist = distribution[bin][rgb];

   for (rgb = 0; rgb < 4; rgb++)                                                 //  R/G/B/white (all)
   {
      if (! rgbw[rgb]) continue;                                                 //  color not selected for graph
      if (rgb == 0) cairo_set_source_rgb(cr,1,0,0);
      if (rgb == 1) cairo_set_source_rgb(cr,0,1,0);
      if (rgb == 2) cairo_set_source_rgb(cr,0,0,1);
      if (rgb == 3) cairo_set_source_rgb(cr,0,0,0);                              //  color "white" = R+G+B uses black line

      cairo_move_to(cr,0,winhh-1);                                               //  start at (0,0)

      for (px = 0; px < winww; px++)                                             //  x from 0 to window width
      {
         bin = Nbins * px / winww;                                               //  bin = 0-Nbins for x = 0-width
         py = 0.9 * winhh * distribution[bin][rgb] / maxdist;                    //  height of bin in window
         py = winhh * sqrt(1.0 * py / winhh);
         py = winhh - py - 1;
         cairo_line_to(cr,px,py);                                                //  draw line from bin to bin
      }

      cairo_stroke(cr);
   }

   return;
}


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

//  Paint a horizontal stripe drawing area with a color progressing from
//  black to white. This represents a brightness scale from 0 to 255.

void brightness_scale(GtkWidget *drawarea, cairo_t *cr, int *)
{
   int      px, ww, hh;
   float    fbright;

   ww = gtk_widget_get_allocated_width(drawarea);                                //  drawing area size
   hh = gtk_widget_get_allocated_height(drawarea);

   for (px = 0; px < ww; px++)                                                   //  draw brightness scale
   {
      fbright = 1.0 * px / ww;
      cairo_set_source_rgb(cr,fbright,fbright,fbright);
      cairo_move_to(cr,px,0);
      cairo_line_to(cr,px,hh-1);
      cairo_stroke(cr);
   }

   return;
}


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

//  magnify image within a given radius of dragged mouse

namespace magnify_names
{
   int   magnify_dialog_event(zdialog* zd, cchar *event);
   void  magnify_mousefunc();
   void  magnify_dopixels(int ftf);

   float       Xmag;                                                             //  magnification, 1 - 5x
   int         Mx, My;                                                           //  mouse location, image space
   int         Mrad;                                                             //  mouse radius
}


//  menu function

void m_magnify(GtkWidget *, cchar *)
{
   using namespace magnify_names;

   F1_help_topic = "magnify image";

   cchar *   mess = E2X("Drag mouse on image. \n"
                        "Left click to cancel.");

   /***
             __________________________
            |    Magnify Image         |
            |                          |
            |  Drag mouse on image.    |
            |  Left click to cancel.   |
            |                          |
            |  radius  [_____]         |
            |  X-size  [_____]         |
            |                          |
            |                [cancel]  |
            |__________________________|

   ***/

   m_viewmode(0,"F");                                                            //  file view mode                     19.0
   if (! curr_file) return;

   if (zd_magnify) {                                                             //  toggle magnify mode 
      zdialog_send_event(zd_magnify,"kill");
      return;
   }

   else {
      zdialog *zd = zdialog_new(E2X("Magnify Image"),Mwin,Bcancel,null);
      zd_magnify = zd;

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

      zdialog_add_widget(zd,"hbox","hbr","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","labr","hbr",Bradius,"space=5");
      zdialog_add_widget(zd,"zspin","Mrad","hbr","50|500|10|200");
      zdialog_add_widget(zd,"hbox","hbx","dialog",0,"space=3");
      zdialog_add_widget(zd,"label","labx","hbx",E2X("X-size"),"space=5");
      zdialog_add_widget(zd,"zspin","Xmag","hbx","2|10|1|2");

      zdialog_fetch(zd,"Mrad",Mrad);                                             //  initial mouse radius
      zdialog_fetch(zd,"Xmag",Xmag);                                             //  initial magnification

      zdialog_resize(zd,200,0);
      zdialog_restore_inputs(zd);                                                //  preload prior user inputs
      zdialog_run(zd,magnify_dialog_event,"save");                               //  run dialog, parallel

      zdialog_send_event(zd,"Mrad");                                             //  initializations
      zdialog_send_event(zd,"Xmag");
   }

   takeMouse(magnify_mousefunc,dragcursor);                                      //  connect mouse function
   return;
}


//  dialog event and completion callback function

int magnify_names::magnify_dialog_event(zdialog *zd, cchar *event)
{
   using namespace magnify_names;
   
   if (strmatch(event,"kill")) zd->zstat = 1;                                    //  from slide show 

   if (zd->zstat) {                                                              //  terminate
      zd_magnify = 0;
      zdialog_free(zd);
      freeMouse();
      return 1;
   }

   if (strmatch(event,"focus"))                                                  //  toggle mouse capture
      takeMouse(magnify_mousefunc,dragcursor);

   if (strmatch(event,"Mrad")) {
      zdialog_fetch(zd,"Mrad",Mrad);                                             //  new mouse radius
      return 1;
   }

   if (strmatch(event,"Xmag")) {
      zdialog_fetch(zd,"Xmag",Xmag);                                             //  new magnification
      return 1;
   }

   return 1;
}


//  pixel paint mouse function

void magnify_names::magnify_mousefunc()
{
   using namespace magnify_names;

   static int     ftf = 1;
   
   if (! curr_file) return;
   if (FGWM != 'F') return;
   if (! zd_magnify) return;
   
   if (Mxdown) Fpaint2();                                                        //  drag start, erase prior if any
   Mxdown = 0;

   if (Mxdrag || Mydrag)                                                         //  drag in progress
   {
      Mx = Mxdrag;                                                               //  save mouse position
      My = Mydrag;
      Mxdrag = Mydrag = 0;
      magnify_dopixels(ftf);                                                     //  magnify pixels inside mouse
      gdk_window_set_cursor(gdkwin,blankcursor);
      ftf = 0;
   }

   else
   {
      if (! ftf) Fpaint2();                                                      //  refresh image
      ftf = 1;
      gdk_window_set_cursor(gdkwin,dragcursor);
   }

   return;
}


//  Get pixels from mouse circle within full size image Fpxb,
//    magnify and move into Mpxb, paint. 

void magnify_names::magnify_dopixels(int ftf)
{
   using namespace magnify_names;
   
   int         Frad;                                                             //  mouse radius, image space
   int         Fx, Fy, Fww, Fhh;                                                 //  mouse circle, enclosing box, Fpxb
   static int  pFx, pFy, pFww, pFhh;
   PIXBUF      *pxb1, *pxb2, *pxbx;
   int         ww1, hh1, rs1, ww2, hh2, rs2;
   uint8       *pixels1, *pixels2, *pix1, *pix2;
   int         nch = Fpxb->nc;
   int         px1, py1, px2, py2;
   int         xlo, xhi, ylo, yhi;
   int         xc, yc, dx, dy;
   float       R2, R2lim;
   cairo_t     *cr;
   
   Frad = Mrad / Mscale;                                                         //  keep magnify circle constant       20.0

   //  get box enclosing PRIOR mouse circle, restore those pixels
   
   if (! ftf)                                                                    //  continuation of mouse drag
   {
      pxb1 = gdk_pixbuf_new_subpixbuf(Fpxb->pixbuf,pFx,pFy,pFww,pFhh);           //  unmagnified pixels, Fpxb
      if (! pxb1) return;

      ww1 = pFww * Mscale;                                                       //  scale to Mpxb
      hh1 = pFhh * Mscale;
      pxbx = gdk_pixbuf_scale_simple(pxb1,ww1,hh1,BILINEAR);
      g_object_unref(pxb1);
      pxb1 = pxbx;
      
      px1 = pFx * Mscale;                                                        //  copy into Mpxb
      py1 = pFy * Mscale;
      gdk_pixbuf_copy_area(pxb1,0,0,ww1,hh1,Mpxb->pixbuf,px1,py1);

      g_object_unref(pxb1);
   }

   //  get box enclosing current mouse circle in Fpxb

   Fx = Mx - Frad;                                                               //  mouse circle, enclosing box
   Fy = My - Frad;                                                               //  (Fpxb, 1x image)
   Fww = Fhh = Frad * 2;
   
   //  clip current mouse box to keep within image

   if (Fx < 0) {
      Fww += Fx;
      Fx = 0;
   }
   
   if (Fy < 0) {
      Fhh += Fy;
      Fy = 0;
   }
   
   if (Fx + Fww > Fpxb->ww) 
      Fww = Fpxb->ww - Fx;

   if (Fy + Fhh > Fpxb->hh)
      Fhh = Fpxb->hh - Fy;
      
   if (Fww <= 0 || Fhh <= 0) return;
   
   pFx = Fx;                                                                     //  save this box for next time
   pFy = Fy;
   pFww = Fww;
   pFhh = Fhh;

   //  scale box for Mpxb, then magnify by Xmag
   
   pxb1 = gdk_pixbuf_new_subpixbuf(Fpxb->pixbuf,Fx,Fy,Fww,Fhh);
   if (! pxb1) return;
   
   ww1 = Fww * Mscale;
   hh1 = Fhh * Mscale;
   pxbx = gdk_pixbuf_scale_simple(pxb1,ww1,hh1,BILINEAR);
   g_object_unref(pxb1);
   pxb1 = pxbx;                                                                  //  unmagnified pixels scaled to Mpxb
   rs1 = gdk_pixbuf_get_rowstride(pxb1);
   pixels1 = gdk_pixbuf_get_pixels(pxb1);
   
   ww2 = ww1 * Xmag;
   hh2 = hh1 * Xmag;
   pxb2 = gdk_pixbuf_scale_simple(pxb1,ww2,hh2,BILINEAR);                        //  corresp. magnified pixels
   rs2 = gdk_pixbuf_get_rowstride(pxb2);
   pixels2 = gdk_pixbuf_get_pixels(pxb2);
   
   //  copy magnified pixels within mouse radius only
   
   xlo = (ww2 - ww1) / 2;                                                        //  pxb2 overlap area with pxb1
   xhi = ww2 - xlo;
   ylo = (hh2 - hh1) / 2;
   yhi = hh2 - ylo;

   xc = (Mx - Fx) * Mscale;                                                      //  mouse center in pxb1
   yc = (My - Fy) * Mscale;
   R2lim = Frad * Mscale;                                                        //  mouse radius in pxb1
   
   for (py2 = ylo; py2 < yhi; py2++)                                             //  loop pxb2 pixels
   for (px2 = xlo; px2 < xhi; px2++)
   {
      px1 = px2 - xlo;                                                           //  corresp. pxb1 pixel
      py1 = py2 - ylo;
      if (px1 < 0 || px1 >= ww1) continue;
      if (py1 < 0 || py1 >= hh1) continue;
      dx = px1 - xc;
      dy = py1 - yc;
      R2 = sqrtf(dx * dx + dy * dy);
      if (R2 > R2lim) continue;                                                  //  outside mouse radius
      pix1 = pixels1 + py1 * rs1 + px1 * nch;
      pix2 = pixels2 + py2 * rs2 + px2 * nch;
      memcpy(pix1,pix2,nch);
   }

   px1 = Fx * Mscale;                                                            //  copy into Mpxb
   py1 = Fy * Mscale;
   gdk_pixbuf_copy_area(pxb1,0,0,ww1,hh1,Mpxb->pixbuf,px1,py1);

   g_object_unref(pxb1);
   g_object_unref(pxb2);

   cr = draw_context_create(gdkwin,draw_context);
   if (! cr) return;

   Fpaint4(Fx,Fy,Fww,Fhh,cr);

   draw_mousecircle(Mx,My,Frad,0,cr);

   draw_context_destroy(draw_context);

   return;
}


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

//  find all duplicated files and create corresponding gallery of duplicates

namespace duplicates_names
{
   int         thumbsize;
   int         Nfiles;
   char        **files;
   int         Fallfiles, Fgallery;
}


//  menu function

void m_duplicates(GtkWidget *, const char *) 
{
   using namespace duplicates_names;

   int  duplicates_dialog_event(zdialog *zd, cchar *event);
   void randomize();

   PIXBUF      *pxb;
   GError      *gerror;
   uint8       *pixels, *pix1;
   uint8       *pixelsii, *pixelsjj, *pixii, *pixjj;
   FILE        *fid = 0;
   zdialog     *zd;
   int         Ndups = 0;                                                        //  image file and duplicate counts
   int         thumbsize, pixdiff, pixcount;
   int         zstat, ii, jj, kk, cc, err, ndup;
   int         ww, hh, rs, px, py;
   int         trgb, diff, sumdiff;
   int         percent;
   char        text[100], *pp;
   char        tempfile[200], albumfile[200];

   typedef struct                                                                //  thumbnail data
   {
      int      trgb;                                                             //  mean RGB sum
      int      ww, hh, rs;                                                       //  pixel dimensions
      char     *file;                                                            //  file name
      uint8    *pixels;                                                          //  image pixels
   }  Tdata_t;

   Tdata_t     **Tdata = 0;

   F1_help_topic = "find duplicates";
   if (checkpend("all")) return;                                                 //  check nothing pending
   m_viewmode(0,"G");                                                            //  gallery view mode                  19.0

   if (Findexvalid == 0) {
      zmessageACK(Mwin,Bnoindex);                                                //  no image index                     20.0
      return;
   }

   free_resources();
   Fblock = 1;
   
   //   randomize();          1-shot


/***
       _______________________________________________
      |         Find Duplicate Images                 |
      |                                               |
      | (o) all files   (o) current gallery           |
      | File count: nnnn                              |
      |                                               |
      | thumbnail size [ 64 ]  [calculate]            |
      | pixel difference [ 2 ]  pixel count [ 2 ]     |
      |                                               |
      | Thumbnails: nnnnnn nn%   Duplicates: nnn nn%  |
      |                                               |
      | /topfolder/subfolder1/subfolder2/...          |
      | imagefile.jpg                                 |
      |                                               |
      |                            [proceed] [cancel] |
      |_______________________________________________|

***/

   zd = zdialog_new(E2X("Find Duplicate Images"),Mwin,Bproceed,Bcancel,null);
   
   zdialog_add_widget(zd,"hbox","hbwhere","dialog",0,"space=5");
   zdialog_add_widget(zd,"radio","allfiles","hbwhere",E2X("All files"),"space=3");
   zdialog_add_widget(zd,"radio","gallery","hbwhere",E2X("Current gallery"),"space=8");
   
   zdialog_add_widget(zd,"hbox","hbfiles","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labfiles","hbfiles",E2X("File count:"),"space=3");
   zdialog_add_widget(zd,"label","filecount","hbfiles","0");
   
   zdialog_add_widget(zd,"hbox","hbthumb","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labthumb","hbthumb",E2X("Thumbnail size"),"space=3");
   zdialog_add_widget(zd,"zspin","thumbsize","hbthumb","32|512|16|256","space=3");
   zdialog_add_widget(zd,"zbutton","calculate","hbthumb",Bcalculate,"space=5");  

   zdialog_add_widget(zd,"hbox","hbdiff","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labdiff","hbdiff",E2X("Pixel difference"),"space=3");
   zdialog_add_widget(zd,"zspin","pixdiff","hbdiff","1|20|1|1","space=3");
   zdialog_add_widget(zd,"label","space","hbdiff",0,"space=8");
   zdialog_add_widget(zd,"label","labsum","hbdiff",E2X("Pixel count"),"space=3");
   zdialog_add_widget(zd,"zspin","pixcount","hbdiff","1|999|1|1","space=3");

   zdialog_add_widget(zd,"hbox","hbstats","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labthumbs1","hbstats","Thumbnails:","space=3");
   zdialog_add_widget(zd,"label","thumbs","hbstats","0");
   zdialog_add_widget(zd,"label","Tpct","hbstats","0%","space=3");
   zdialog_add_widget(zd,"label","space","hbstats",0,"space=5");
   zdialog_add_widget(zd,"label","labdups1","hbstats",E2X("Duplicates:"),"space=3");
   zdialog_add_widget(zd,"label","dups","hbstats","0");
   zdialog_add_widget(zd,"label","Dpct","hbstats","0%","space=3");

   zdialog_add_widget(zd,"hbox","hbfolder","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","currfolder","hbfolder"," ","space=8");
   zdialog_add_widget(zd,"hbox","hbfile","dialog");
   zdialog_add_widget(zd,"label","currfile","hbfile"," ","space=8");

   zdialog_stuff(zd,"allfiles",1);                                               //  default all files
   Fallfiles = 1;

   files = (char **) zmalloc(maximages * sizeof(char *));

   for (ii = jj = 0; ii < Nxxrec; ii++)                                          //  count all files
   {
      pp = xxrec_tab[ii]->file;
      pp = image2thumbfile(pp);                                                  //  omit those without a thumbnail
      if (! pp) continue;
      files[jj++] = pp;
   }
   
   Nfiles = jj;

   snprintf(text,20,"%d",Nfiles);                                                //  file count >> dialog
   zdialog_stuff(zd,"filecount",text);

   zdialog_run(zd,duplicates_dialog_event,"parent");                             //  run dialog
   zstat = zdialog_wait(zd);                                                     //  wait for user inputs
   if (zstat != 1) goto cleanup;                                                 //  canceled 

   if (Nfiles < 2) {
      zmessageACK(Mwin," <2 images");
      goto cleanup;
   }
   
   zdialog_fetch(zd,"thumbsize",thumbsize);                                      //  thumbnail size to use
   zdialog_fetch(zd,"pixdiff",pixdiff);                                          //  pixel difference threshold
   zdialog_fetch(zd,"pixcount",pixcount);                                        //  pixel count threshold
   
   cc = Nfiles * sizeof(Tdata_t);                                                //  allocate memory for thumbnail data
   Tdata = (Tdata_t **) zmalloc(cc);
   memset(Tdata,0,cc); 
   
   Ffuncbusy = 1;
   
   for (ii = 0; ii < Nfiles; ii++)                                               //  screen out thumbnails not 
   {                                                                             //    matching an image file
      pp = thumb2imagefile(files[ii]);
      if (pp) zfree(pp);
      else {
         zfree(files[ii]);
         files[ii] = 0;
      }
   }

   for (ii = 0; ii < Nfiles; ii++)                                               //  read thumbnails into memory
   {
      if (Fkillfunc) goto cleanup;

      if (! files[ii]) continue;

      Tdata[ii] = (Tdata_t *) zmalloc(sizeof(Tdata_t));
      Tdata[ii]->file = files[ii];                                               //  thumbnail file
      files[ii] = 0;

      kk = thumbsize;
      gerror = 0;                                                                //  read thumbnail >> pixbuf
      pxb = gdk_pixbuf_new_from_file_at_size(Tdata[ii]->file,kk,kk,&gerror);
      if (! pxb) {
         printz("file: %s \n %s",Tdata[ii]->file,gerror->message);
         zfree(Tdata[ii]->file);
         zfree(Tdata[ii]);
         Tdata[ii] = 0;
         continue;
      }

      ww = gdk_pixbuf_get_width(pxb);                                            //  pixbuf dimensions
      hh = gdk_pixbuf_get_height(pxb);
      rs = gdk_pixbuf_get_rowstride(pxb);
      pixels = gdk_pixbuf_get_pixels(pxb);

      Tdata[ii]->ww = ww;                                                        //  thumbnail dimensions
      Tdata[ii]->hh = hh;
      Tdata[ii]->rs = rs;
      cc = rs * hh;
      Tdata[ii]->pixels = (uint8 *) zmalloc(cc);
      memcpy(Tdata[ii]->pixels,pixels,cc);                                       //  thumbnail pixels
      
      trgb = 0;                                                                  //  compute mean RGB sum
      for (py = 0; py < hh; py++)
      for (px = 0; px < ww; px++) {
         pix1 = pixels + py * rs + px * 3;
         trgb += pix1[0] + pix1[1] + pix1[2];
      }
      Tdata[ii]->trgb = trgb / ww / hh;                                          //  thumbnail mean RGB sum

      g_object_unref(pxb);                                                       //  bugfix                             19.0

      snprintf(text,100,"%d",ii);                                                //  stuff thumbs read into dialog
      zdialog_stuff(zd,"thumbs",text);

      percent = 100.0 * ii / Nfiles;                                             //  and percent read
      snprintf(text,20,"%02d %c",percent,'%');
      zdialog_stuff(zd,"Tpct",text);

      zmainloop();                                                               //  keep GTK alive
   }
   
   for (ii = jj = 0; ii < Nfiles; ii++)                                          //  remove empty members of Tdata[]
      if (Tdata[ii]) Tdata[jj++] = Tdata[ii];
   Nfiles = jj;                                                                  //  new count

   for (ii = 0; ii < Nfiles; ii++)                                               //  replace thumbnail filespecs
   {                                                                             //    with corresp. image filespecs
      if (! Tdata[ii]) continue;
      pp = thumb2imagefile(Tdata[ii]->file);
      zfree(Tdata[ii]->file);
      Tdata[ii]->file = pp;
   }
   
   snprintf(tempfile,200,"%s/duplicate_images",temp_folder);                     //  open file for gallery output
   fid = fopen(tempfile,"w");
   if (! fid) goto filerror;
   
   Ndups = 0;                                                                    //  total duplicates

   for (ii = 0; ii < Nfiles; ii++)                                               //  loop all thumbnails ii
   {
      zmainloop();

      if (Fkillfunc) goto cleanup;

      percent = 100.0 * (ii+1) / Nfiles;                                         //  show percent processed
      snprintf(text,20,"%02d %c",percent,'%');
      zdialog_stuff(zd,"Dpct",text);

      if (! Tdata[ii]) continue;                                                 //  removed from list

      pp = strrchr(Tdata[ii]->file,'/');
      if (! pp) continue;
      *pp = 0;
      zdialog_stuff(zd,"currfolder",Tdata[ii]->file);                            //  update folder and file
      zdialog_stuff(zd,"currfile",pp+1);                                         //    in dialog
      *pp = '/';
      
      trgb = Tdata[ii]->trgb;
      ww = Tdata[ii]->ww;
      hh = Tdata[ii]->hh;
      rs = Tdata[ii]->rs;
      pixelsii = Tdata[ii]->pixels;

      ndup = 0;                                                                  //  file duplicates

      for (jj = ii+1; jj < Nfiles; jj++)                                         //  loop all thumbnails jj
      {
         if (! Tdata[jj]) continue;                                              //  removed from list
         
         if (abs(trgb - Tdata[jj]->trgb) > 1) continue;                          //  brightness not matching
         if (ww != Tdata[jj]->ww) continue;                                      //  size not matching
         if (hh != Tdata[jj]->hh) continue;

         pixelsjj = Tdata[jj]->pixels;
         sumdiff = 0;

         for (py = 0; py < hh; py++)
         for (px = 0; px < ww; px++)
         {
            pixii = pixelsii + py * rs + px * 3;
            pixjj = pixelsjj + py * rs + px * 3;

            diff = abs(pixii[0] - pixjj[0]) 
                 + abs(pixii[1] - pixjj[1]) 
                 + abs(pixii[2] - pixjj[2]);
            if (diff < pixdiff) continue;                                        //  pixels match within threshold

            sumdiff++;                                                           //  count unmatched pixels
            if (sumdiff >= pixcount) {                                           //  if over threshold,
               py = hh; px = ww; }                                               //    break out both loops
         }
         
         if (sumdiff >= pixcount) continue;                                      //  thumbnails not matching

         if (ndup == 0) {
            fprintf(fid,"%s\n",Tdata[ii]->file);                                 //  first duplicate, output file name
            ndup++;
            Ndups++;
         }

         fprintf(fid,"%s\n",Tdata[jj]->file);                                    //  output duplicate image file name
         zfree(Tdata[jj]->file);                                                 //  remove from list
         zfree(Tdata[jj]->pixels);
         zfree(Tdata[jj]);
         Tdata[jj] = 0;
         ndup++;
         Ndups++;

         snprintf(text,100,"%d",Ndups);                                          //  update total duplicates found
         zdialog_stuff(zd,"dups",text);

         zmainloop();
      }
   }

   fclose(fid);
   fid = 0;
   
   if (Ndups == 0) {
      zmessageACK(Mwin,"0 duplicates");
      goto cleanup;
   }

   navi::gallerytype = SEARCH;                                                   //  generate gallery of duplicate images
   gallery(tempfile,"initF",0);
   gallery(0,"paint",0);                                                         //  position at top
   m_viewmode(0,"G");
   
   snprintf(albumfile,200,"%s/Duplicate Images",albums_folder);                  //  save search results in the
   err = copyFile(tempfile,albumfile);                                           //    album "Duplicate Images"
   if (err) zmessageACK(Mwin,strerror(err));

cleanup:

   zdialog_free(zd);

   if (fid) fclose(fid);
   fid = 0;

   if (files) {
      for (ii = 0; ii < Nfiles; ii++)
         if (files[ii]) zfree(files[ii]);
      zfree(files);
   }

   if (Tdata) {   
      for (ii = 0; ii < Nfiles; ii++) {
         if (! Tdata[ii]) continue;
         zfree(Tdata[ii]->file);
         zfree(Tdata[ii]->pixels);
         zfree(Tdata[ii]);
      }
      zfree(Tdata);
   }

   Ffuncbusy = 0;
   Fkillfunc = 0;
   Fblock = 0;
   return;

filerror:
   zmessageACK(Mwin,"file error: %s",strerror(errno));
   goto cleanup;
}


//  dialog event and completion function

int  duplicates_dialog_event(zdialog *zd, cchar *event)
{
   using namespace duplicates_names;

   double      freemem, cachemem, reqmem;
   char        text[20], *pp, *pp2;
   int         nn, ii, jj;

   if (strmatch(event,"allfiles"))
   {
      zdialog_fetch(zd,"allfiles",nn);
      Fallfiles = nn;
      if (! Fallfiles) return 1;

      for (ii = jj = 0; ii < Nxxrec; ii++)                                       //  count all files
      {
         pp = xxrec_tab[ii]->file;
         pp = image2thumbfile(pp);                                               //  omit those without thumbnail
         if (! pp) continue;
         files[jj++] = pp;
      }
      
      Nfiles = jj;

      snprintf(text,20,"%d",Nfiles);                                             //  file count >> dialog
      zdialog_stuff(zd,"filecount",text);
      return 1;
   }

   if (strmatch(event,"gallery"))
   {
      zdialog_fetch(zd,"gallery",nn);
      Fgallery = nn;
      if (! Fgallery) return 1;

      for (ii = jj = 0; ii < navi::Nfiles; ii++)                                 //  scan current gallery
      {
         pp = gallery(0,"get",ii);
         if (! pp) break;
         if (*pp == '!') {                                                       //  skip folders
            zfree(pp);
            continue;
         }
         
         pp2 = image2thumbfile(pp);                                              //  get corresp. thumbnail file
         zfree(pp);
         if (! pp2) continue;
         files[jj++] = pp2;                                                      //  save thumbnail
      }
      
      Nfiles = jj;

      snprintf(text,20,"%d",Nfiles);                                             //  file count >> dialog
      zdialog_stuff(zd,"filecount",text);
      return 1;
   }
   
   if (strmatch(event,"calculate"))                                              //  calculate thumbnail size
   {
      zd->zstat = 0;                                                             //  keep dialog active

      parseprocfile("/proc/meminfo","MemFree:",&freemem,0);                      //  get amount of free memory
      parseprocfile("/proc/meminfo","Cached:",&cachemem,0);
      freemem += cachemem;                                                       //  KB
      freemem *= 1024;
      
      for (thumbsize = 16; thumbsize <= 512; thumbsize += 8) {                   //  find largest thumbnail size
         reqmem = 0.7 * thumbsize * thumbsize * 3 * Nfiles;                      //    that probably works
         if (reqmem > freemem) break;
      }
      
      thumbsize -= 8;                                                            //  biggest size that fits
      if (thumbsize < 16) {
         zmessageACK(Mwin,E2X("too many files, cannot continue"));
         return 1;
      }

      zdialog_stuff(zd,"thumbsize",thumbsize);                                   //  stuff into dialog
      return 1;
   }

   if (! zd->zstat) return 1;                                                    //  wait for user input
   if (zd->zstat != 1) Fkillfunc = 1;                                            //  [cancel] or [x]
   return 1;                                                                     //  [proceed]
}


//  Make small random changes to all images.
//  Used for testing and benchmarking Find Duplicates. 

void randomize()
{
   using namespace duplicates_names;

   char     *file;
   int      px, py;
   int      ii, jj, kk;
   float    *pixel;
   
   for (ii = 0; ii < Nxxrec; ii++)                                               //  loop all files
   {
      if (drandz() > 0.95) continue;                                             //  leave 5% duplicates
      
      zmainloop();                                                               //  keep GTK alive

      file = zstrdup(xxrec_tab[ii]->file);
      if (! file) continue;

      printf(" %d  %s \n",ii,file);                                              //  log progress
      
      f_open(file);                                                              //  open and read file
      E0pxm = PXM_load(file,1);
      if (! E0pxm) continue;

      jj = 2 + 49 * drandz();                                                    //  random 2-50 pixels

      for (kk = 0; kk < jj; kk++)
      {
         px = E0pxm->ww * drandz();                                              //  random pixel
         py = E0pxm->hh * drandz();
         pixel = PXMpix(E0pxm,px,py);
         pixel[0] = pixel[1] = pixel[2] = 0;                                     //  RGB = black
      }
      
      f_save(file,"jpg",8,0,1);                                                  //  write file
      
      PXM_free(E0pxm);
      zfree(file);
   }

   return;
}


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

//  Show RGB values for 1-9 pixels selected with mouse-clicks.
//  Additional pixel position tracks active mouse position

void  show_RGB_mousefunc();
int   show_RGB_timefunc(void *);

zdialog     *RGBSzd;
int         RGBSpixel[10][2];                                                    //  0-9 clicked pixels + current mouse
int         RGBSnpix = 0;                                                        //  no. clicked pixels, 0-9
int         RGBSdelta = 0;                                                       //  abs/delta mode
int         RGBSlabels = 0;                                                      //  pixel labels on/off


void m_show_RGB(GtkWidget *, cchar *)
{
   int   show_RGB_event(zdialog *zd, cchar *event);

   cchar       *mess = E2X("Click image to select pixels.");
   cchar       *header = " Pixel            Red     Green   Blue";
   char        hbx[8] = "hbx", pixx[8] = "pixx";                                 //  last char. is '0' to '9'
   int         ii;

   F1_help_topic = "show RGB";

   m_viewmode(0,"F");                                                            //  file view mode                     19.0
   if (! curr_file) return;                                                      //  no image file

   if (! E0pxm && ! E1pxm && ! E3pxm) {
      E0pxm = PXM_load(curr_file,1);                                             //  never edited 
      if (! E0pxm) return;                                                       //  get poss. 16-bit file              19.5
      curr_file_bpc = f_load_bpc;
   }

/***
    _________________________________________
   |                                         |
   |  Click image to select pixels.          |
   |  [x] delta  [x] labels                  |
   |                                         |                                   //  remove EV option                   19.2
   |   Pixel           Red    Green  Blue    |
   |   A xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   B xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   C xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   D xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   E xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   F xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   G xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   H xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |   I xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |     xxxxx xxxxx   xxxxx  xxxxx  xxxxx   |
   |                                         |
   |                        [clear] [done]   |
   |_________________________________________|

***/

   RGBSnpix = 0;                                                                 //  no clicked pixels yet
   RGBSlabels = 0;                                                               //  no labels yet                      19.5

   if (RGBSzd) zdialog_free(RGBSzd);                                             //  delete previous if any
   zdialog *zd = zdialog_new(E2X("Show RGB"),Mwin,Bclear,Bdone,null);
   RGBSzd = zd;

   zdialog_add_widget(zd,"hbox","hbmess","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labmess","hbmess",mess,"space=5");

   zdialog_add_widget(zd,"hbox","hbmym","dialog");
   zdialog_add_widget(zd,"check","delta","hbmym","delta","space=8");
   zdialog_add_widget(zd,"check","labels","hbmym","labels","space=8");

   if (RGBSdelta && E3pxm) zdialog_stuff(zd,"delta",1);

   zdialog_add_widget(zd,"vbox","vbdat","dialog",0,"space=5");                   //  vbox for pixel values
   zdialog_add_widget(zd,"hbox","hbpix","vbdat");
   zdialog_add_widget(zd,"label","labheader","hbpix",header);                    //  Pixel        Red    Green  Blue
   zdialog_labelfont(zd,"labheader","monospace 9",header);

   for (ii = 0; ii < 10; ii++)
   {                                                                             //  10 hbox's with 10 labels
      hbx[2] = '0' + ii;
      pixx[3] = '0' + ii;
      zdialog_add_widget(zd,"hbox",hbx,"vbdat");
      zdialog_add_widget(zd,"label",pixx,hbx);
   }

   zdialog_run(zd,show_RGB_event,"save");                                        //  run dialog
   takeMouse(show_RGB_mousefunc,dotcursor);                                      //  connect mouse function
   g_timeout_add(200,show_RGB_timefunc,0);                                       //  start timer function, 200 ms

   return;
}


//  dialog event function

int show_RGB_event(zdialog *zd, cchar *event)
{
   if (zd->zstat) {
      if (zd->zstat == 1) {                                                      //  clear
         zd->zstat = 0;                                                          //  keep dialog active
         RGBSnpix = 0;                                                           //  clicked pixel count = 0
         erase_toptext(102);                                                     //  erase labels on image
      }
      else {                                                                     //  done or kill
         freeMouse();                                                            //  disconnect mouse function
         zdialog_free(RGBSzd);                                                   //  kill dialog
         RGBSzd = 0;
         erase_toptext(102);
      }
      Fpaint2();
      return 0;                                                                  //  19.0
   }

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

   if (strmatch(event,"delta")) {                                                //  set absolute/delta mode
      zdialog_fetch(zd,"delta",RGBSdelta);
      if (RGBSdelta && ! E3pxm) {
         RGBSdelta = 0;                                                          //  block delta mode if no edit underway
         zdialog_stuff(zd,"delta",0);
         zmessageACK(Mwin,E2X("Edit function must be active"));
      }
   }

   if (strmatch(event,"labels"))                                                 //  get labels on/off
      zdialog_fetch(zd,"labels",RGBSlabels);

   return 0;
}


//  mouse function
//  fill table positions 0-8 with last clicked pixel positions and RGB data
//  next table position tracks current mouse position and RGB data

void show_RGB_mousefunc()                                                        //  mouse function
{
   int      ii;
   PXM      *pxm;

   if (E3pxm) pxm = E3pxm;                                                       //  report image being edited
   else if (E1pxm) pxm = E1pxm;
   else if (E0pxm) pxm = E0pxm;
   else return;                                                                  //  must have E0/E1/E3                 19.5

   if (Mxposn <= 0 || Mxposn >= pxm->ww-1) return;                               //  mouse outside image, ignore
   if (Myposn <= 0 || Myposn >= pxm->hh-1) return;

   if (LMclick)                                                                  //  left click, add labeled position
   {
      LMclick = 0;

      if (RGBSnpix == 9) {                                                       //  if all 9 labeled positions filled,
         for (ii = 1; ii < 9; ii++) {                                            //    remove first (oldest) and
            RGBSpixel[ii-1][0] = RGBSpixel[ii][0];                               //      push the rest back
            RGBSpixel[ii-1][1] = RGBSpixel[ii][1];
         }
         RGBSnpix = 8;                                                           //  position for newest clicked pixel
      }

      ii = RGBSnpix;                                                             //  labeled position to fill, 0-8
      RGBSpixel[ii][0] = Mxclick;                                                //  save newest pixel
      RGBSpixel[ii][1] = Myclick;
      RGBSnpix++;                                                                //  count is 1-9
   }
   
   ii = RGBSnpix;                                                                //  fill last position from active mouse
   RGBSpixel[ii][0] = Mxposn;
   RGBSpixel[ii][1] = Myposn;
   
   return;
}


//  timer function
//  display RGB values for last 0-9 clicked pixels and current mouse position

int show_RGB_timefunc(void *arg)
{
   static char    label[9][4] = { " A ", " B ", " C ", " D ", " E ",             //  labels A-I for last 0-9 clicked pixels
                                  " F ", " G ", " H ", " I " };
   PXM         *pxm = 0;
   int         ii, jj, px, py;
   int         ww, hh;
   float       red3, green3, blue3;
   float       *ppixa, *ppixb;
   char        text[100], pixx[8] = "pixx";
   
   if (! RGBSzd) return 0;                                                       //  user quit, cancel timer
   if (! curr_file) return 0;

   if (! E0pxm && ! E1pxm && ! E3pxm) {
      E0pxm = PXM_load(curr_file,1); 
      if (! E0pxm) return 0;                                                     //  get poss. 16-bit file
      curr_file_bpc = f_load_bpc;
   }

   if (E3pxm) pxm = E3pxm;                                                       //  report image being edited
   else if (E1pxm) pxm = E1pxm;
   else if (E0pxm) pxm = E0pxm;
   else return 0;

   if (RGBSdelta && ! E3pxm) {
      RGBSdelta = 0;                                                             //  delta mode only if edit active
      zdialog_stuff(RGBSzd,"delta",RGBSdelta);                                   //  update dialog                      19.2
   }

   ww = pxm->ww;
   hh = pxm->hh;
   
   for (ii = 0; ii < RGBSnpix; ii++)                                             //  0-9 clicked pixels                 19.2
   {
      px = RGBSpixel[ii][0];                                                     //  next pixel to report
      py = RGBSpixel[ii][1];
      if (px >= 0 && px < ww && py >= 0 && py < hh) continue;                    //  within image limits
      
      for (jj = ii+1; jj < RGBSnpix + 1; jj++) {
         RGBSpixel[jj-1][0] = RGBSpixel[jj][0];                                  //  remove pixel outside limits
         RGBSpixel[jj-1][1] = RGBSpixel[jj][1];                                  //    and pack the remaining down
      }                                                                          //  include last+1 = curr. mouse position

      ii--;
      RGBSnpix--;
   }
   
   erase_toptext(102);

   if (RGBSlabels) {
      for (ii = 0; ii < RGBSnpix; ii++) {                                        //  show pixel labels on image
         px = RGBSpixel[ii][0];
         py = RGBSpixel[ii][1];
         add_toptext(102,px,py,label[ii],"Sans 8");
      }
   }
   
   for (ii = 0; ii < 10; ii++)                                                   //  loop positions 0 to 9
   {
      pixx[3] = '0' + ii;                                                        //  widget names "pix0" ... "pix9"

      if (ii > RGBSnpix) {                                                       //  no pixel there yet
         zdialog_stuff(RGBSzd,pixx,"");                                          //  blank report line
         continue;
      }

      px = RGBSpixel[ii][0];                                                     //  next pixel to report
      py = RGBSpixel[ii][1];

      ppixa = PXMpix(pxm,px,py);                                                 //  get pixel RGB values
      red3 = ppixa[0];
      green3 = ppixa[1];
      blue3 = ppixa[2];

      if (RGBSdelta) {                                                           //  delta RGB for edited image         19.2
         ppixb = PXMpix(E1pxm,px,py);                                            //  "before" image E1
         red3 -= ppixb[0];
         green3 -= ppixb[1];
         blue3 -= ppixb[2];
      }
      
      if (ii == RGBSnpix)                                                        //  last table position
         snprintf(text,100,"   %5d %5d  ",px,py);                                //  mouse pixel, format "   xxxx yyyy"
      else snprintf(text,100," %c %5d %5d  ",'A'+ii,px,py);                      //  clicked pixel, format " A xxxx yyyy"

      snprintf(text+14,86,"   %6.2f  %6.2f  %6.2f ",red3,green3,blue3);
      zdialog_labelfont(RGBSzd,pixx,"monospace 9",text);
   }

   Fpaint2();

   return 1;
}


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

//  convert color profile of current image

editfunc    EFcolorprof;
char        ICCprofilename[100];                                                 //  new color profile name 
char        colorprof1[200] = "/usr/share/color/icc/colord/AdobeRGB1998.icc";
char        colorprof2[200] = "/usr/share/color/icc/colord/sRGB.icc";


//  menu function

void m_color_profile(GtkWidget *, cchar *menu)
{
   int colorprof_dialog_event(zdialog *zd, cchar *event);

   zdialog     *zd;
   int         err;
   cchar       *exifkey[2], *exifdata[2];

   F1_help_topic = "color profile";
   m_viewmode(0,"0");                                                            //  file view mode                     19.0

   EFcolorprof.menuname = menu;
   EFcolorprof.menufunc = m_color_profile;
   EFcolorprof.funcname = "color_profile";
   EFcolorprof.Frestart = 1;                                                     //  allow restart
   EFcolorprof.Fscript = 1;                                                      //  scripting supported
   if (! edit_setup(EFcolorprof)) return;                                        //  setup edit
   
   *ICCprofilename = 0;                                                          //  no color profile change

/***
       ________________________________________________________
      |       Change Color Profile                             |
      |                                                        |
      |  input profile  [___________________________] [Browse] |
      |  output profile [___________________________] [Browse] |
      |                                                        |
      |                                [Apply] [Done] [Cancel] |
      |________________________________________________________|

***/

   zd = zdialog_new(E2X("Change Color Profile"),Mwin,Bapply,Bdone,Bcancel,null);
   EFcolorprof.zd = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lab1","hb1",E2X("input profile"),"space=5");
   zdialog_add_widget(zd,"zentry","prof1","hb1",0,"expand|size=30");
   zdialog_add_widget(zd,"button","butt1","hb1",Bbrowse,"space=5");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","lab2","hb2",E2X("output profile"),"space=5");
   zdialog_add_widget(zd,"zentry","prof2","hb2",0,"expand|size=30");
   zdialog_add_widget(zd,"button","butt2","hb2",Bbrowse,"space=5");

   zdialog_stuff(zd,"prof1",colorprof1);
   zdialog_stuff(zd,"prof2",colorprof2);

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

   zdialog_wait(zd);                                                             //  wait for completion
   if (! *ICCprofilename) return;                                                //  no color profile change
   
   m_file_save_version(0,0);                                                     //  save as new version and re-open
   shell_quiet("rm -f %s/undo_*",temp_folder);                                   //  remove undo/redo files
   URS_pos = URS_max = 0;                                                        //  reset undo/redo stack

   exifkey[0] = exif_colorprof2_key;                                             //  remove embedded color profile
   exifdata[0] = "";
   exifkey[1] = exif_colorprof1_key;                                             //  set new color profile name
   exifdata[1] = ICCprofilename;
   err = exif_put(curr_file,exifkey,exifdata,2);
   if (err) zmessageACK(Mwin,E2X("Unable to change EXIF color profile"));

   zmessageACK(Mwin,E2X("automatic new version created"));
   return;
}


//  dialog event and completion callback function

int colorprof_dialog_event(zdialog *zd, cchar *event)
{
   cchar    *title = E2X("color profile");
   char     *file;
   float    *fpix1, *fpix2;
   float    f256 = 1.0 / 256.0;
   uint     Npix, nn;
   
   cmsHTRANSFORM  cmsxform;
   cmsHPROFILE    cmsprof1, cmsprof2;

   if (strmatch(event,"apply")) zd->zstat = 1;                                   //  from script
   if (strmatch(event,"done")) zd->zstat = 2;                                    //  from edit_setup() or f_save()
   if (strmatch(event,"cancel")) zd->zstat = 3;                                  //  from f_open()
   
   if (strmatch(event,"butt1")) {
      zdialog_fetch(zd,"prof1",colorprof1,200);                                  //  select input profile
      file = zgetfile(title,MWIN,"file",colorprof1);
      if (! file) return 1;
      zdialog_stuff(zd,"prof1",file);
      zfree(file);
   }

   if (strmatch(event,"butt2")) {
      zdialog_fetch(zd,"prof2",colorprof2,200);                                  //  select output profile
      file = zgetfile(title,MWIN,"file",colorprof2);
      if (! file) return 1;
      zdialog_stuff(zd,"prof2",file);
      zfree(file);
   }

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

   if (zd->zstat == 1) zd->zstat = 0;                                            //  [apply] - keep dialog open

   if (zd->zstat)
   {
      if (zd->zstat == 2 && CEF->Fmods) edit_done(0);                            //  commit edit
      else {
         edit_cancel(0);                                                         //  discard edit
         *ICCprofilename = 0;                                                    //  no ICC profile change
      }
      return 1;
   }

   zdialog_fetch(zd,"prof1",colorprof1,200);                                     //  [apply] - get final profiles
   zdialog_fetch(zd,"prof2",colorprof2,200);

   cmsprof1 = cmsOpenProfileFromFile(colorprof1,"r");
   if (! cmsprof1) {
      zmessageACK(Mwin,E2X("unknown cms profile %s"),colorprof1);
      return 1;
   }

   cmsprof2 = cmsOpenProfileFromFile(colorprof2,"r");
   if (! cmsprof2) {
      zmessageACK(Mwin,E2X("unknown cms profile %s"),colorprof2);
      return 1;
   }

   //  calculate the color space transformation table

   Ffuncbusy = 1;
   zmainsleep(0.2);

   cmsxform = cmsCreateTransform(cmsprof1,TYPE_RGB_FLT,cmsprof2,TYPE_RGB_FLT,INTENT_PERCEPTUAL,0);
   if (! cmsxform) {
      zmessageACK(Mwin,"cmsCreateTransform() failed");
      Ffuncbusy = 0;
      return 1;
   }

   fpix1 = E0pxm->pixels;                                                        //  input and output pixels
   fpix2 = E3pxm->pixels;
   Npix = E0pxm->ww * E0pxm->hh;

   for (uint ii = 0; ii < 3 * Npix; ii++)                                        //  rescale to range 0 - 0.9999
      fpix2[ii] = f256 * fpix1[ii];

   while (Npix)                                                                  //  convert image pixels
   {
      nn = Npix;
      if (nn > 100000) nn = 100000;                                              //  do 100K per call
      cmsDoTransform(cmsxform,fpix2,fpix2,nn);                                   //  speed: 3 megapixels/sec for 3 GHz CPU
      fpix2 += nn * 3;
      Npix -= nn;
      zmainloop(20);                                                             //  keep GTK alive
   }

   fpix2 = E3pxm->pixels;
   Npix = E0pxm->ww * E0pxm->hh;
   for (uint ii = 0; ii < 3 * Npix; ii++) {                                      //  rescale back to 0 - 255.99
      fpix2[ii] = fpix2[ii] * 256.0;
      if (fpix2[ii] > 255.9) fpix2[ii] = 255.9;
      if (fpix2[ii] < 0) fpix2[ii] = 0;                                          //  compensate cms bug
   }

   cmsInfoType it = (cmsInfoType) 0;
   cmsGetProfileInfoASCII(cmsprof2,it,"en","US",ICCprofilename,100);             //  new color profile name

   cmsDeleteTransform(cmsxform);                                                 //  free resources
   cmsCloseProfile(cmsprof1);
   cmsCloseProfile(cmsprof2);

   Ffuncbusy = 0;
   CEF->Fmods++;                                                                 //  image is modified
   CEF->Fsaved = 0;
   Fpaint2();                                                                    //  update window image

   return 1;
}


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

//  printer color calibration tool

namespace calibprint
{
   int   dialog_event(zdialog *zd, cchar *event);
   void  printchart();
   void  scanchart();
   void  fixchart();
   void  processchart();

//  parameters for RGB step size of 23: 0 23 46 69 ... 253 (253 --> 255)
//  NC    colors per RGB dimension (12) (counting both 0 and 255)                //  step size from 16 to 23
//  CS    color step size (23) 
//  TS    tile size in pixels (70)
//  ROWS  chart rows (50)               ROWS x COLS must be >= NC*NC*NC
//  COLS  chart columns (35)

   #define NC 12
   #define CS 23
   #define TS 70
   #define ROWS 50
   #define COLS 35

   #define NC2 (NC*NC)
   #define NC3 (NC*NC*NC)

   int   RGBvals[NC];
   int   Ntiles = NC3;
   int   chartww = COLS * TS;             //  chart image size
   int   charthh = ROWS * TS;
   int   margin = 80;                     //  chart margins
   char  printchartfile[200];
   char  scanchartfile[200];
}


//  menu function

void m_calibrate_printer(GtkWidget *, cchar *menu)
{
   using namespace calibprint;
   
   zdialog     *zd;
   cchar       *title = E2X("Calibrate Printer");

   F1_help_topic = "calibrate printer";
   m_viewmode(0,"F");                                                            //  file view mode                     19.0

   for (int ii = 0; ii < NC; ii++)                                               //  construct RGBvals table
      RGBvals[ii] = CS * ii;
   RGBvals[NC-1] = 255;                                                          //  set last value = 255

/***
       ______________________________________
      |         Calibrate Printer            |
      |                                      |
      |  (o) print color chart               |
      |  (o) scan and save color chart       |
      |  (o) align and trim color chart      |
      |  (o) open and process color chart    |
      |  (o) print image with revised colors |
      |                                      |
      |                  [Proceed] [Cancel]  |
      |______________________________________|

***/

   zd = zdialog_new(title,Mwin,Bproceed,Bcancel,null);

   zdialog_add_widget(zd,"radio","printchart","dialog",E2X("print color chart"));
   zdialog_add_widget(zd,"radio","scanchart","dialog",E2X("scan and save color chart"));
   zdialog_add_widget(zd,"radio","fixchart","dialog",E2X("align and trim color chart"));
   zdialog_add_widget(zd,"radio","processchart","dialog",E2X("open and process color chart"));
   zdialog_add_widget(zd,"radio","printimage","dialog",E2X("print image with revised colors"));

   zdialog_stuff(zd,"printchart",1);
   zdialog_stuff(zd,"scanchart",0);
   zdialog_stuff(zd,"fixchart",0);
   zdialog_stuff(zd,"processchart",0);
   zdialog_stuff(zd,"printimage",0);
   
   zdialog_resize(zd,250,0);
   zdialog_run(zd,dialog_event,"parent");
   return;
}


//  dialog event and completion function

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

   int      nn;
   
   F1_help_topic = "calibrate printer";

   if (! zd->zstat) return 1;                                                    //  wait for [proceed] or [cancel]

   if (zd->zstat != 1) {                                                         //  cancel
      zdialog_free(zd);
      return 1;
   }
   
   zdialog_fetch(zd,"printchart",nn);
   if (nn) {
      printchart();
      return 1;
   }
   
   zdialog_fetch(zd,"scanchart",nn);
   if (nn) {
      scanchart();
      return 1;
   }
   
   zdialog_fetch(zd,"fixchart",nn);
   if (nn) {
      fixchart();
      return 1;
   }
   
   zdialog_fetch(zd,"processchart",nn);
   if (nn) {
      processchart();
      return 1;
   }

   zdialog_fetch(zd,"printimage",nn);
   if (nn) {
      print_calibrated();
      return 1;
   }
   
   zd->zstat = 0;                                                                //  keep dialog active
   return 1;
}


//  generate and print the color chart

void calibprint::printchart()
{
   using namespace calibprint;

   int      ii, cc, fww, fhh, rx, gx, bx;
   int      row, col, px, py;
   int      chartrs;
   uint8    *chartpixels, *pix1;
   PIXBUF   *chartpxb;
   GError   *gerror = 0;
   
   fww = chartww + 2 * margin;
   fhh = charthh + 2 * margin;

   chartpxb = gdk_pixbuf_new(GDKRGB,0,8,fww,fhh);                                //  make chart image
   if (! chartpxb) {
      zmessageACK(Mwin,"cannot create pixbuf");
      return;
   }

   chartpixels = gdk_pixbuf_get_pixels(chartpxb);                                //  clear to white
   chartrs = gdk_pixbuf_get_rowstride(chartpxb);
   cc = fhh * chartrs;
   memset(chartpixels,255,cc);
   
   for (py = 0; py < charthh; py++)                                              //  fill chart tiles with colors
   for (px = 0; px < chartww; px++)
   {
      row = py / TS;
      col = px / TS;
      ii = row * COLS + col;
      if (ii >= Ntiles) break;                                                   //  last chart positions may be unused
      rx = ii / NC2;
      gx = (ii - NC2 * rx) / NC;                                                 //  RGB index values for tile ii
      bx = ii - NC2 * rx - NC * gx;
      pix1 = chartpixels + (py + margin) * chartrs + (px + margin) * 3;
      pix1[0] = RGBvals[rx];
      pix1[1] = RGBvals[gx];
      pix1[2] = RGBvals[bx];
   }
   
   for (py = margin-10; py < fhh-margin+10; py++)                                //  add green margin around tiles
   for (px = margin-10; px < fww-margin+10; px++)                                //    for easier de-skew and trim
   {
      if (py > margin-1 && py < fhh-margin &&
          px > margin-1 && px < fww-margin) continue;
      pix1 = chartpixels + py * chartrs + px * 3;
      pix1[0] = pix1[2] = 0;
      pix1[1] = 255;
   }
   
   snprintf(printchartfile,200,"%s/printchart.png",printer_color_folder);
   gdk_pixbuf_save(chartpxb,printchartfile,"png",&gerror,null);
   if (gerror) {
      zmessageACK(Mwin,gerror->message);
      return;
   }
   
   g_object_unref(chartpxb);

   zmessageACK(Mwin,"Print chart in vertical orientation without margins.");
   print_image_file(Mwin,printchartfile);                                        //  print the chart

   return;
}


//  scan the color chart

void calibprint::scanchart()
{
   using namespace calibprint;

   zmessageACK(Mwin,E2X("Scan the printed color chart. \n"
                        "The darkest row is at the top. \n"
                        "Save in %s/"),printer_color_folder);
   return;
}


//  edit and fix the color chart

void calibprint::fixchart()
{
   using namespace calibprint;
   
   char     *pp;

   zmessageACK(Mwin,E2X("Open and edit the scanned color chart file. \n"
                        "Remove any skew or rotation from scanning. \n"
                        "(Use the Fix Perspective function for this). \n"
                        "Cut off the thin green margin ACCURATELY."));
   
   pp = zgetfile("scanned color chart file",MWIN,"file",printer_color_folder,1);
   if (! pp) return;
   strncpy0(scanchartfile,pp,200);
   f_open(scanchartfile,0,0,1,0);
   return;
}


//  process the scanned and fixed color chart

void calibprint::processchart()
{
   using namespace calibprint;

   PIXBUF   *chartpxb;
   GError   *gerror = 0;
   uint8    *chartpixels, *pix1;
   FILE     *fid;
   char     mapfile[200], *pp, *pp2;
   int      chartrs, chartnc, px, py;
   int      ii, nn, row, col, rx, gx, bx;
   int      xlo, xhi, ylo, yhi;
   float    fww, fhh;
   int      Rsum, Gsum, Bsum, Rout, Gout, Bout;
   int      r1, r2, ry, g1, g2, gy, b1, b2, by;
   int      ERR1[NC][NC][NC][3], ERR2[NC][NC][NC][3];

   zmessageACK(Mwin,E2X("Open the trimmed color chart file"));

   pp = zgetfile("trimmed color chart file",MWIN,"file",printer_color_folder,1);
   if (! pp) return;
   strncpy0(scanchartfile,pp,200);
   
   chartpxb = gdk_pixbuf_new_from_file(scanchartfile,&gerror);                   //  scanned chart without margins
   if (! chartpxb) {
      if (gerror) zmessageACK(Mwin,gerror->message);
      return;
   }
   
   chartww = gdk_pixbuf_get_width(chartpxb);
   charthh = gdk_pixbuf_get_height(chartpxb);
   chartpixels = gdk_pixbuf_get_pixels(chartpxb);
   chartrs = gdk_pixbuf_get_rowstride(chartpxb);
   chartnc = gdk_pixbuf_get_n_channels(chartpxb);
   fww = 1.0 * chartww / COLS;
   fhh = 1.0 * charthh / ROWS;

   for (row = 0; row < ROWS; row++)                                              //  loop each tile
   for (col = 0; col < COLS; col++)
   {
      ii = row * COLS + col;
      if (ii >= Ntiles) break;

      ylo = row * fhh;                                                           //  tile position within chart image
      yhi = ylo + fhh;
      xlo = col * fww;
      xhi = xlo + fww;

      Rsum = Gsum = Bsum = nn = 0;

      for (py = ylo+fhh/5; py < yhi-fhh/5; py++)                                 //  get tile pixels less 20% margins
      for (px = xlo+fww/5; px < xhi-fww/5; px++)
      {
         pix1 = chartpixels + py * chartrs + px * chartnc;
         Rsum += pix1[0];
         Gsum += pix1[1];
         Bsum += pix1[2];
         nn++;
      }

      Rout = Rsum / nn;                                                          //  average tile RGB values
      Gout = Gsum / nn;
      Bout = Bsum / nn;

      rx = ii / NC2;
      gx = (ii - NC2 * rx) / NC;
      bx = ii - NC2 * rx - NC * gx;
      
      ERR1[rx][gx][bx][0] = Rout - RGBvals[rx];                                  //  error = (scammed RGB) - (printed RGB)
      ERR1[rx][gx][bx][1] = Gout - RGBvals[gx];
      ERR1[rx][gx][bx][2] = Bout - RGBvals[bx];
   }

   g_object_unref(chartpxb);

   //  anneal the error values to reduce randomness
   
   for (int pass = 1; pass <= 4; pass++)                                         //  4 passes
   {
      for (rx = 0; rx < NC; rx++)                                                //  use neighbors in 3 channels
      for (gx = 0; gx < NC; gx++)
      for (bx = 0; bx < NC; bx++)
      {
         r1 = rx-1;
         r2 = rx+1;
         g1 = gx-1;
         g2 = gx+1;
         b1 = bx-1;
         b2 = bx+1;

         if (r1 < 0) r1 = 0;
         if (r2 > NC-1) r2 = NC-1;
         if (g1 < 0) g1 = 0;
         if (g2 > NC-1) g2 = NC-1;
         if (b1 < 0) b1 = 0;
         if (b2 > NC-1) b2 = NC-1;
         
         Rsum = Gsum = Bsum = nn = 0;

         for (ry = r1; ry <= r2; ry++)
         for (gy = g1; gy <= g2; gy++)
         for (by = b1; by <= b2; by++)
         {
            Rsum += ERR1[ry][gy][by][0];
            Gsum += ERR1[ry][gy][by][1];
            Bsum += ERR1[ry][gy][by][2];
            nn++;
         }

         ERR2[rx][gx][bx][0] = Rsum / nn;
         ERR2[rx][gx][bx][1] = Gsum / nn;
         ERR2[rx][gx][bx][2] = Bsum / nn;
      }

      for (rx = 0; rx < NC; rx++)
      for (gx = 0; gx < NC; gx++)
      for (bx = 0; bx < NC; bx++)
      {
         ERR1[rx][gx][bx][0] = ERR2[rx][gx][bx][0];
         ERR1[rx][gx][bx][1] = ERR2[rx][gx][bx][1];
         ERR1[rx][gx][bx][2] = ERR2[rx][gx][bx][2];
      }

      for (rx = 1; rx < NC-1; rx++)                                              //  use neighbors in same channel
      for (gx = 0; gx < NC; gx++)
      for (bx = 0; bx < NC; bx++)
         ERR2[rx][gx][bx][0] = 0.5 * (ERR1[rx-1][gx][bx][0] + ERR1[rx+1][gx][bx][0]);

      for (rx = 0; rx < NC; rx++)
      for (gx = 1; gx < NC-1; gx++)
      for (bx = 0; bx < NC; bx++)
         ERR2[rx][gx][bx][1] = 0.5 * (ERR1[rx][gx-1][bx][1] + ERR1[rx][gx+1][bx][1]);

      for (rx = 0; rx < NC; rx++)
      for (gx = 0; gx < NC; gx++)
      for (bx = 1; bx < NC-1; bx++)
         ERR2[rx][gx][bx][2] = 0.5 * (ERR1[rx][gx][bx-1][2] + ERR1[rx][gx][bx+1][2]);

      for (rx = 0; rx < NC; rx++)
      for (gx = 0; gx < NC; gx++)
      for (bx = 0; bx < NC; bx++)
      {
         ERR1[rx][gx][bx][0] = ERR2[rx][gx][bx][0];
         ERR1[rx][gx][bx][1] = ERR2[rx][gx][bx][1];
         ERR1[rx][gx][bx][2] = ERR2[rx][gx][bx][2];
      }
   }                                                                             //  pass loop

   //  save finished color map to user-selected file

   zmessageACK(Mwin,E2X("Set the name for the output calibration file \n"
                        "[your calibration name].dat"));

   snprintf(mapfile,200,"%s/%s",printer_color_folder,colormapfile);
   pp = zgetfile("Color Map File",MWIN,"save",mapfile,1);
   if (! pp) return;
   pp2 = strrchr(pp,'/');
   zfree(colormapfile);
   colormapfile = zstrdup(pp2+1);
   save_params();
   zfree(pp);
   
   snprintf(mapfile,200,"%s/%s",printer_color_folder,colormapfile);
   fid = fopen(mapfile,"w");
   if (! fid) return;

   for (rx = 0; rx < NC; rx++)
   for (gx = 0; gx < NC; gx++)
   for (bx = 0; bx < NC; bx++)
   {
      fprintf(fid,"RGB: %3d %3d %3d   ERR: %4d %4d %4d \n",
          RGBvals[rx], RGBvals[gx], RGBvals[bx],
          ERR2[rx][gx][bx][0], ERR2[rx][gx][bx][1], ERR2[rx][gx][bx][2]);
   }

   fclose(fid);

   return;
}


//  Print the current image file with adjusted colors
//  Also called from the file menu function m_print_calibrated()

void print_calibrated()
{
   using namespace calibprint;

   zdialog  *zd;
   int      zstat;
   cchar    *title = E2X("Color map file to use");
   char     mapfile[200];
   FILE     *fid;
   PIXBUF   *pixbuf;
   GError   *gerror = 0;
   uint8    *pixels, *pix1;
   char     *pp, *pp2, printfile[100];
   int      ww, hh, rs, nc, px, py, nn, err;
   int      R1, G1, B1, R2, G2, B2;
   int      RGB[NC][NC][NC][3], ERR[NC][NC][NC][3];
   int      rr1, rr2, gg1, gg2, bb1, bb2;
   int      rx, gx, bx;
   int      ii, Dr, Dg, Db;
   float    W[8], w, Wsum, D, Dmax, F;
   float    Er, Eg, Eb;
   
   F1_help_topic = "print calibrated";
   m_viewmode(0,"F");                                                            //  file view mode                     19.0

   if (! curr_file) {
      zmessageACK(Mwin,E2X("Select the image file to print."));
      return;
   }
   
   for (int ii = 0; ii < NC; ii++)                                               //  construct RGBvals table
      RGBvals[ii] = CS * ii;
   RGBvals[NC-1] = 255;                                                          //  set last value = 255

   zd = zdialog_new(title,Mwin,Bbrowse,Bproceed,Bcancel,null);                   //  show current color map file 
   zdialog_add_widget(zd,"hbox","hbmap","dialog");                               //    and allow user to choose another
   zdialog_add_widget(zd,"label","labmap","hbmap",0,"space=3");
   zdialog_stuff(zd,"labmap",colormapfile);
   zdialog_resize(zd,250,0);
   zdialog_run(zd,0,"parent");
   zstat = zdialog_wait(zd);
   zdialog_free(zd);

   if (zstat == 1) {                                                             //  [browse]
      snprintf(mapfile,200,"%s/%s",printer_color_folder,colormapfile);
      pp = zgetfile("Color Map File",MWIN,"file",mapfile,1);
      if (! pp) return;
      pp2 = strrchr(pp,'/');
      if (colormapfile) zfree(colormapfile);
      colormapfile = zstrdup(pp2+1);
      zfree(pp);
   }
   
   else if (zstat != 2) return;                                                  //  not proceed: cancel
   
   snprintf(mapfile,200,"%s/%s",printer_color_folder,colormapfile);
   fid = fopen(mapfile,"r");                                                     //  read color map file
   if (! fid) return;
   
   for (R1 = 0; R1 < NC; R1++)
   for (G1 = 0; G1 < NC; G1++)
   for (B1 = 0; B1 < NC; B1++)
   {
      nn = fscanf(fid,"RGB: %d %d %d   ERR: %d %d %d ",
           &RGB[R1][G1][B1][0], &RGB[R1][G1][B1][1], &RGB[R1][G1][B1][2],
           &ERR[R1][G1][B1][0], &ERR[R1][G1][B1][1], &ERR[R1][G1][B1][2]);
      if (nn != 6) {
         zmessageACK(Mwin,E2X("file format error"));
         fclose(fid);
         return;
      }
   }
   
   fclose(fid);
   
   pixbuf = gdk_pixbuf_copy(Fpxb->pixbuf);                                       //  get image pixbuf to convert
   if (! pixbuf) {
      if (gerror) zmessageACK(Mwin,gerror->message);
      return;
   }
   
   if (checkpend("all")) return;
   Fblock = 1;
   Ffuncbusy = 1;

   ww = gdk_pixbuf_get_width(pixbuf);
   hh = gdk_pixbuf_get_height(pixbuf);
   pixels = gdk_pixbuf_get_pixels(pixbuf);
   rs = gdk_pixbuf_get_rowstride(pixbuf);
   nc = gdk_pixbuf_get_n_channels(pixbuf);
   
   poptext_window(MWIN,E2X("converting colors..."),300,200,0,-1);

   for (py = 0; py < hh; py++)
   for (px = 0; px < ww; px++)
   {
      pix1 = pixels + py * rs + px * nc;
      R1 = pix1[0];                                                              //  image RGB values
      G1 = pix1[1];
      B1 = pix1[2];
      
      rr1 = R1/CS;                                                               //  get color map values surrounding RGB
      rr2 = rr1 + 1;
      if (rr2 > NC-1) {                                                          //  if > last entry, use last entry
         rr1--; 
         rr2--; 
      }

      gg1 = G1/CS;
      gg2 = gg1 + 1;
      if (gg2 > NC-1) { 
         gg1--; 
         gg2--; 
      }

      bb1 = B1/CS;
      bb2 = bb1 + 1;
      if (bb2 > NC-1) { 
         bb1--; 
         bb2--; 
      }

      ii = 0;
      Wsum = 0;
      Dmax = CS;
      
      for (rx = rr1; rx <= rr2; rx++)                                            //  loop 8 enclosing error map nodes
      for (gx = gg1; gx <= gg2; gx++)
      for (bx = bb1; bx <= bb2; bx++)
      {
         Dr = R1 - RGBvals[rx];                                                  //  RGB distance from enclosing node
         Dg = G1 - RGBvals[gx];
         Db = B1 - RGBvals[bx];
         D = sqrtf(Dr*Dr + Dg*Dg + Db*Db);
         if (D > Dmax) W[ii] = 0;
         else W[ii] = (Dmax - D) / Dmax;                                         //  weight of node
         Wsum += W[ii];                                                          //  sum of weights
         ii++;
      }
      
      ii = 0;
      Er = Eg = Eb = 0;

      for (rx = rr1; rx <= rr2; rx++)                                            //  loop 8 enclosing error map nodes
      for (gx = gg1; gx <= gg2; gx++)
      for (bx = bb1; bx <= bb2; bx++)
      {
         w = W[ii] / Wsum;
         Er += w * ERR[rx][gx][bx][0];                                           //  weighted sum of map node errors 
         Eg += w * ERR[rx][gx][bx][1];
         Eb += w * ERR[rx][gx][bx][2];
         ii++;
      }
      
      F = 1.0;                                                                   //  use 100% of calculated error
      R2 = R1 - F * Er;                                                          //  adjusted RGB = image RGB - error
      G2 = G1 - F * Eg;
      B2 = B1 - F * Eb;

      if (R2 < 0) R2 = 0;
      if (G2 < 0) G2 = 0;
      if (B2 < 0) B2 = 0;
      
      if (R2 > 255) R2 = 255;                                                    //  preserving RGB ratio does not help
      if (G2 > 255) G2 = 255;
      if (B2 > 255) B2 = 255;

      pix1[0] = R2;
      pix1[1] = G2;
      pix1[2] = B2;
      
      zmainloop(100);                                                            //  keep GTK alive
   }
   
   poptext_killnow();

   Ffuncbusy = 0;
   Fblock = 0;

   snprintf(printfile,100,"%s/printfile.png",temp_folder);                       //  save revised pixbuf to print file
   gdk_pixbuf_save(pixbuf,printfile,"png",&gerror,"compression","1",null);
   if (gerror) {
      zmessageACK(Mwin,gerror->message);
      return;
   }
   
   g_object_unref(pixbuf);
   
   err = f_open(printfile,0,0,1,0);                                              //  open print file
   if (err) return;

   zmessageACK(Mwin,E2X("Image colors are converted for printing."));
   print_image_file(Mwin,printfile);

   return;
}


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

//  setup x and y grid lines - count/spacing, enable/disable, offsets

void m_gridlines(GtkWidget *widget, cchar *menu)
{
   int gridlines_dialog_event(zdialog *zd, cchar *event);

   zdialog     *zd;
   int         G;

   m_viewmode(0,"F");                                                            //  file view mode                     19.0

   if (menu && strmatchN(menu,"grid ",5))                                        //  grid N from some edit functions
      currgrid = menu[5] - '0';

   else if (! widget) {                                                          //  from KB shortcut
      toggle_grid(2);                                                            //  toggle grid lines on/off
      return;
   }

   else currgrid = 0;                                                            //  must be menu call

   if (currgrid == 0)
      F1_help_topic = "grid lines";

/***
       ____________________________________________
      |      Grid Lines                            |
      |                                            |
      | x-spacing [____]    y-spacing [____]       |
      |  x-count  [____]     y-count  [____]       |
      | x-enable  [_]       y-enable  [_]          |
      |                                            |
      | x-offset =================[]=============  |
      | y-offset ==============[]================  |
      |                                            |
      |                                     [Done] |
      |____________________________________________|

***/

   zd = zdialog_new(E2X("Grid Lines"),Mwin,Bdone,null);

   zdialog_add_widget(zd,"hbox","hb0","dialog",0,"space=10");
   zdialog_add_widget(zd,"vbox","vb1","hb0",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb0",0,"homog");
   zdialog_add_widget(zd,"vbox","vbspace","hb0",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb3","hb0",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb4","hb0",0,"homog");

   zdialog_add_widget(zd,"label","lab1x","vb1",E2X("x-spacing"));
   zdialog_add_widget(zd,"label","lab2x","vb1",E2X("x-count"));
   zdialog_add_widget(zd,"label","lab4x","vb1",E2X("x-enable"));

   zdialog_add_widget(zd,"zspin","spacex","vb2","20|200|1|50","space=2");
   zdialog_add_widget(zd,"zspin","countx","vb2","0|100|1|2","space=2");
   zdialog_add_widget(zd,"check","enablex","vb2",0);

   zdialog_add_widget(zd,"label","lab1y","vb3",E2X("y-spacing"));
   zdialog_add_widget(zd,"label","lab2y","vb3",E2X("y-count"));
   zdialog_add_widget(zd,"label","lab4y","vb3",E2X("y-enable"));

   zdialog_add_widget(zd,"zspin","spacey","vb4","20|200|1|50");
   zdialog_add_widget(zd,"zspin","county","vb4","0|100|1|2");
   zdialog_add_widget(zd,"check","enabley","vb4",0);

   zdialog_add_widget(zd,"hbox","hboffx","dialog");
   zdialog_add_widget(zd,"label","lab3x","hboffx",Bxoffset,"space=7");
   zdialog_add_widget(zd,"hscale","offsetx","hboffx","0|100|1|0","expand");
   zdialog_add_widget(zd,"label","space","hboffx",0,"space=20");

   zdialog_add_widget(zd,"hbox","hboffy","dialog");
   zdialog_add_widget(zd,"label","lab3y","hboffy",Byoffset,"space=7");
   zdialog_add_widget(zd,"hscale","offsety","hboffy","0|100|1|0","expand");
   zdialog_add_widget(zd,"label","space","hboffy",0,"space=20");

   G = currgrid;                                                                 //  grid logic overhaul

   if (! gridsettings[G][GXS])                                                   //  if [G] never set, get defaults
      for (int ii = 0; ii < 9; ii++)
         gridsettings[G][ii] = gridsettings[0][ii];

   zdialog_stuff(zd,"enablex",gridsettings[G][GX]);                              //  current settings >> dialog widgets
   zdialog_stuff(zd,"enabley",gridsettings[G][GY]);
   zdialog_stuff(zd,"spacex",gridsettings[G][GXS]);
   zdialog_stuff(zd,"spacey",gridsettings[G][GYS]);
   zdialog_stuff(zd,"countx",gridsettings[G][GXC]);
   zdialog_stuff(zd,"county",gridsettings[G][GYC]);
   zdialog_stuff(zd,"offsetx",gridsettings[G][GXF]);
   zdialog_stuff(zd,"offsety",gridsettings[G][GYF]);

   zdialog_set_modal(zd);
   zdialog_run(zd,gridlines_dialog_event,"parent");
   zdialog_wait(zd);
   zdialog_free(zd);
   return;
}


//  dialog event function

int gridlines_dialog_event(zdialog *zd, cchar *event)
{
   int      G = currgrid;

   if (zd->zstat)                                                                //  done or cancel
   {
      if (zd->zstat != 1) return 1;
      if (gridsettings[G][GX] || gridsettings[G][GY])
         gridsettings[G][GON] = 1;
      else gridsettings[G][GON] = 0;
      Fpaint2();
      return 1;
   }

   if (strmatch(event,"enablex"))                                                //  x/y grid enable or disable
      zdialog_fetch(zd,"enablex",gridsettings[G][GX]);

   if (strmatch(event,"enabley"))
      zdialog_fetch(zd,"enabley",gridsettings[G][GY]);

   if (strmatch(event,"spacex"))                                                 //  x/y grid spacing (if counts == 0)
      zdialog_fetch(zd,"spacex",gridsettings[G][GXS]);

   if (strmatch(event,"spacey"))
      zdialog_fetch(zd,"spacey",gridsettings[G][GYS]);

   if (strmatch(event,"countx"))                                                 //  x/y grid line counts
      zdialog_fetch(zd,"countx",gridsettings[G][GXC]);

   if (strmatch(event,"county"))
      zdialog_fetch(zd,"county",gridsettings[G][GYC]);

   if (strmatch(event,"offsetx"))                                                //  x/y grid starting offsets
      zdialog_fetch(zd,"offsetx",gridsettings[G][GXF]);

   if (strmatch(event,"offsety"))
      zdialog_fetch(zd,"offsety",gridsettings[G][GYF]);

   if (gridsettings[G][GX] || gridsettings[G][GY])                               //  if either grid enabled, show grid
      gridsettings[G][GON] = 1;

   Fpaint2();
   return 1;
}


//  toggle grid lines on or off
//  action: 0 = off, 1 = on, 2 = toggle: on > off, off > on

void toggle_grid(int action)
{
   int   G = currgrid;

   if (action == 0) gridsettings[G][GON] = 0;                                    //  grid off
   if (action == 1) gridsettings[G][GON] = 1;                                    //  grid on
   if (action == 2) gridsettings[G][GON] = 1 - gridsettings[G][GON];             //  toggle grid

   if (gridsettings[G][GON])
      if (! gridsettings[G][GX] && ! gridsettings[G][GY])                        //  if grid on and x/y both off,
         gridsettings[G][GX] = gridsettings[G][GY] = 1;                          //    set both grids on

   Fpaint2();
   return;
}


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

//  choose color for foreground lines
//  (area outline, mouse circle, trim rectangle ...)

void m_line_color(GtkWidget *, cchar *menu)
{
   int line_color_dialog_event(zdialog *zd, cchar *event);

   zdialog  *zd;

   F1_help_topic = "line color";
   m_viewmode(0,"F");                                                            //  file view mode                     19.0

   zd = zdialog_new(E2X("Line Color"),Mwin,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"radio",Bblack,"hb1",Bblack,"space=3");                 //  add radio button per color
   zdialog_add_widget(zd,"radio",Bwhite,"hb1",Bwhite,"space=3");
   zdialog_add_widget(zd,"radio",Bred,"hb1",Bred,"space=3");
   zdialog_add_widget(zd,"radio",Bgreen,"hb1",Bgreen,"space=3");

   zdialog_stuff(zd,Bblack,0);                                                   //  all are initially off
   zdialog_stuff(zd,Bwhite,0);
   zdialog_stuff(zd,Bred,0);
   zdialog_stuff(zd,Bgreen,0);

   if (LINE_COLOR[0] == BLACK[0] && LINE_COLOR[1] == BLACK[1] && LINE_COLOR[2] == BLACK[2])                         //  19.0
      zdialog_stuff(zd,Bblack,1);
   if (LINE_COLOR[0] == WHITE[0] && LINE_COLOR[1] == WHITE[1] && LINE_COLOR[2] == WHITE[2]) 
      zdialog_stuff(zd,Bwhite,1);
   if (LINE_COLOR[0] == RED[0] && LINE_COLOR[1] == RED[1] && LINE_COLOR[2] == RED[2]) 
      zdialog_stuff(zd,Bred,1);
   if (LINE_COLOR[0] == GREEN[0] && LINE_COLOR[1] == GREEN[1] && LINE_COLOR[2] == GREEN[2]) 
      zdialog_stuff(zd,Bgreen,1);

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


//  dialog event and completion function

int line_color_dialog_event(zdialog *zd, cchar *event)
{
   if (strmatch(event,Bblack)) memcpy(LINE_COLOR,BLACK,3*sizeof(int));           //  set selected color                 19.0
   if (strmatch(event,Bwhite)) memcpy(LINE_COLOR,WHITE,3*sizeof(int));
   if (strmatch(event,Bred))   memcpy(LINE_COLOR,RED,3*sizeof(int));
   if (strmatch(event,Bgreen)) memcpy(LINE_COLOR,GREEN,3*sizeof(int));
   if (CEF && CEF->zd) zdialog_send_event(CEF->zd,"line_color");
   Fpaint2();

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


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

//  dark_brite menu function
//  highlight darkest and brightest pixels

namespace darkbrite {
   float    darklim = 0;
   float    brightlim = 255;
}

void m_darkbrite(GtkWidget *, const char *)
{
   using namespace darkbrite;

   int    darkbrite_dialog_event(zdialog* zd, const char *event);

   cchar    *title = E2X("Darkest and Brightest Pixels");

   F1_help_topic = "dark/bright pixels";

   m_viewmode(0,"F");                                                            //  file view mode                     19.0
   if (! curr_file) return;                                                      //  no image file

/**
       ______________________________________
      |    Darkest and Brightest Pixels      |
      |                                      |
      |  Dark Limit   ===[]============ NNN  |
      |  Bright Limit ============[]=== NNN  |
      |                                      |
      |                              [Done]  |
      |______________________________________|

**/

   zdialog *zd = zdialog_new(title,Mwin,Bdone,null);                             //  darkbrite dialog
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"space=3|homog|expand");
   zdialog_add_widget(zd,"vbox","vb3","hb1",0,"homog|space=3");
   zdialog_add_widget(zd,"label","labD","vb1",E2X("Dark Limit"));
   zdialog_add_widget(zd,"label","labB","vb1",E2X("Bright Limit"));
   zdialog_add_widget(zd,"hscale","limD","vb2","0|255|1|0","expand");
   zdialog_add_widget(zd,"hscale","limB","vb2","0|255|1|255","expand");
   zdialog_add_widget(zd,"label","valD","vb3");
   zdialog_add_widget(zd,"label","valB","vb3");

   zdialog_rescale(zd,"limD",0,0,255);
   zdialog_rescale(zd,"limB",0,255,255);
   
   zdialog_stuff(zd,"limD",darklim);                                             //  start with prior values
   zdialog_stuff(zd,"limB",brightlim);

   zdialog_resize(zd,300,0);
   zdialog_run(zd,darkbrite_dialog_event,"save");                                //  run dialog - parallel
   zd_darkbrite = zd;                                                            //  global pointer for Fpaint*()

   zdialog_send_event(zd,"limD");                                                //  initz. NNN labels
   zdialog_send_event(zd,"limB");

   return;
}


//  darkbrite dialog event and completion function

int darkbrite_dialog_event(zdialog *zd, const char *event)                       //  darkbrite dialog event function
{
   using namespace darkbrite;

   char     text[8];

   if (zd->zstat)
   {
      zdialog_free(zd);
      zd_darkbrite = 0;
      Fpaint2();
      return 0;
   }

   if (strmatch(event,"limD")) {
      zdialog_fetch(zd,"limD",darklim);
      snprintf(text,8,"%.0f",darklim);
      zdialog_stuff(zd,"valD",text);
   }

   if (strmatch(event,"limB")) {
      zdialog_fetch(zd,"limB",brightlim);
      snprintf(text,8,"%.0f",brightlim);
      zdialog_stuff(zd,"valB",text);
   }

   Fpaint2();
   return 0;
}


//  this function called by Fpaint() if zd_darkbrite dialog active

void darkbrite_paint()
{
   using namespace darkbrite;

   int         px, py;
   uint8       *pix;
   float       P, D = darklim, B = brightlim;

   for (py = 0; py < Mpxb->hh; py++)                                             //  loop all image pixels
   for (px = 0; px < Mpxb->ww; px++)
   {
      pix = PXBpix(Mpxb,px,py);
      P = pixbright(pix);
      if (P < D) pix[0] = 100;                                                   //  dark pixel = mostly red
      else if (P > B) pix[0] = 0;                                                //  bright pixel = mostly green/blue
   }

   return;                                                                       //  not executed, avoid gcc warning
}


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

//   RAW pixel bias function
//   Aggregate many (>10) gray RAW images made under identical conditions.
//   Average the pixels to detect brightness differences from camera sensors.

namespace map_pixel_bias_names
{
   zdialog  *zd;
   char     bias_map_file[200] = "";
   int      Eww, Ehh;                                                            //  gray image dimensions

   int      Npix;                                                                //  size of pixR/G/B
   float    *pixR, *pixG, *pixB;                                                 //  RGB values by pixel
}


//  menu function

void m_map_pixel_bias(GtkWidget *, cchar *menu)                                  //  20.0
{
   using namespace map_pixel_bias_names;
   
   int map_pixel_bias_dialog_event(zdialog *zd, cchar *event);

   char     text[100];

   F1_help_topic = "map pixel bias";
   if (checkpend("all")) return;

   pixR = 0;

/***
       ___________________________________
      |       Map RAW Pixel Bias          |
      |                                   |
      | [Select Files]  N files selected  |
      |                                   |
      | mean RGB: N.N N.N N.N             |
      | pixel bias: NN.N NN.N NN.N        |
      |                                   |
      |         [measure] [save] [cancel] |
      |___________________________________|

***/

   zd = zdialog_new(E2X("Map RAW Pixel Bias"),Mwin,Bmeasure,Bsave,Bcancel,null);

   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","selectfiles","hbf",Bselectfiles,"space=5");
   zdialog_add_widget(zd,"label","fcount","hbf",0,"space=10");
   
   zdialog_add_widget(zd,"hbox","hbim","dialog");
   zdialog_add_widget(zd,"label","labim","hbim",E2X("mean RGB:"),"space=5");
   zdialog_add_widget(zd,"label","labimdata","hbim","no data","space=5");

   zdialog_add_widget(zd,"hbox","hbpb","dialog");
   zdialog_add_widget(zd,"label","labpb","hbpb","pixel bias:","space=5");
   zdialog_add_widget(zd,"label","labpbdata","hbpb","no data","space=5");
   
   snprintf(text,100,Bfileselected,GScount);                                     //  show selected files count          20.0
   zdialog_stuff(zd,"fcount",text);

   zdialog_run(zd,map_pixel_bias_dialog_event,"parent");
   
   return;
}


//  dialog event and completion callback function

int map_pixel_bias_dialog_event(zdialog *zd, cchar *event)
{
   using namespace map_pixel_bias_names;

   void  map_pixel_bias_measure(zdialog *zd);
   void  map_pixel_bias_save();

   char     countmess[80];

   if (strmatch(event,"selectfiles"))                                            //  select files 
   {
      zdialog_show(zd,0);
      gallery_select();                                                          //  get list of files to convert
      snprintf(countmess,80,Bfileselected,GScount);                              //  update dialog
      zdialog_stuff(zd,"fcount",countmess);
      zdialog_show(zd,1);
   }

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  [measure]
      {
         zd->zstat = 0;
         map_pixel_bias_measure(zd);
         return 1;
      }
      
      else if (zd->zstat == 2)                                                   //  [save]
      {
         zd->zstat = 0;
         map_pixel_bias_save();
         return 1;
      }
      
      else                                                                       //  [cancel] or [x]
      {
         zdialog_free(zd);

         if (pixR) {
            zfree(pixR);
            zfree(pixG);
            zfree(pixB);
         }

         return 1;
      }
   }
   
   return 1;
}


//  aggregate all gray image pixels and measure brightness variations.

void map_pixel_bias_measure(zdialog *zd)
{
   using namespace map_pixel_bias_names;

   PXM      *pxm = 0;
   int      im, Nim, nn;
   int      px, py, qx, qy, ip, iq;
   float    *pix;
   char     text[100];
   double   imageR, imageG, imageB;
   float    neighborR, neighborG, neighborB;
   float    diffR, diffG, diffB;                                                 //  RGB differences
   
   if (GScount < 10) {
      zmessageACK(Mwin,E2X("select at least 10 RAW image files"));
      return;
   }
   
   if (pixR) {
      zfree(pixR);
      zfree(pixG);
      zfree(pixB);
      pixR = 0;
   }
   
   Nim = GScount;                                                                //  image count

   Ffuncbusy = 1;
   Fbusy_goal = Nim;

   for (im = 0; im < Nim; im++)                                                  //  loop all images
   {
      pxm = RAW_PXM_load(GSfiles[im],0,0);                                       //  load image
      if (! pxm) {
         zmessageACK(Mwin,E2X("cannot read file \n %s"),GSfiles[im]);
         goto cleanup;
      }
      
      if (image_file_type(GSfiles[im]) != RAW) {
         zmessageACK(Mwin,E2X("not a RAW file \n %s"),GSfiles[im]);
         goto cleanup;
      }
      
      if (im == 0) {                                                             //  first image
         Eww = pxm->ww;                                                          //  set dimensions for all images
         Ehh = pxm->hh;
         Npix = Eww * Ehh;
         pixR = (float *) zmalloc(Npix * sizeof(float));                         //  allocate memory for RGB pixel sums
         pixG = (float *) zmalloc(Npix * sizeof(float));
         pixB = (float *) zmalloc(Npix * sizeof(float));
      }

      if (pxm->ww != Eww || pxm->hh != Ehh) {                                    //  check image dimensions match
         zmessageACK(Mwin,E2X("dimensions do not match: %s"),GSfiles[im]);
         PXM_free(pxm);
         goto cleanup;
      }

      for (py = 0; py < Ehh; py++)                                               //  sum RGB values by pixel
      for (px = 0; px < Eww; px++)
      {
         pix = PXMpix(pxm,px,py);
         ip = py * Eww + px;
         pixR[ip] += pix[0];
         pixG[ip] += pix[1];
         pixB[ip] += pix[2];
      }

      PXM_free(pxm);
      Fbusy_done++;
      zmainsleep(0.1);
   }
   
   imageR = imageG = imageB = 0;

   for (py = 0; py < Ehh; py++)                                                  //  sum RGB values for all images
   for (px = 0; px < Eww; px++)
   {
      pix = PXMpix(pxm,px,py);
      ip = py * Eww + px;
      imageR += pixR[ip];
      imageG += pixG[ip];
      imageB += pixB[ip];
   }
   
   nn = Eww * Ehh * Nim;                                                         //  no. pixels  x  no. images
   imageR = imageR / nn;                                                         //  mean RGB values for all images
   imageG = imageG / nn;
   imageB = imageB / nn;
   
   snprintf(text,100,"%.2f  %.2f  %.2f",imageR,imageG,imageB);
   zdialog_stuff(zd,"labimdata",text);

   for (py = 0; py < Ehh; py++)                                                  //  mean RGB values by pixel 
   for (px = 0; px < Eww; px++)
   {
      pix = PXMpix(pxm,px,py);
      ip = py * Eww + px;
      pixR[ip] = pixR[ip] / Nim;
      pixG[ip] = pixG[ip] / Nim;
      pixB[ip] = pixB[ip] / Nim;
   }
   
   diffR = diffG = diffB = 0;

   for (py = 2; py < Ehh-2; py++)                                                //  RGB offsets from neighbor pixel mean
   for (px = 2; px < Eww-2; px++)
   {
      neighborR = neighborG = neighborB = 0;

      for (qy = py-2; qy <= py+2; qy++)                                          //  RGB sum of 5x5 block
      for (qx = px-2; qx <= px+2; qx++)
      {
         iq = qy * Eww + qx;
         neighborR += pixR[iq];
         neighborG += pixG[iq];
         neighborB += pixB[iq];
      }
      
      neighborR = neighborR / 25;                                                //  RGB mean of 5x5 neighbor block
      neighborG = neighborG / 25;
      neighborB = neighborB / 25;

      ip = py * Eww + px;
      diffR += fabsf(pixR[ip] - neighborR);                                      //  sum RGB offset from 5x5 block mean
      diffG += fabsf(pixG[ip] - neighborG);
      diffB += fabsf(pixB[ip] - neighborB);
   }

   nn = (Eww-2) * (Ehh-2);
   diffR = diffR / nn;                                                           //  mean RGB offset from 5x5 neighbor block
   diffG = diffG / nn;
   diffB = diffB / nn;

   snprintf(text,100,"%.2f  %.2f  %.2f",diffR,diffG,diffB);
   zdialog_stuff(zd,"labpbdata",text);
   
cleanup:
   Ffuncbusy = 0;
   Fbusy_goal = Fbusy_done = 0;
   return;
}


//  save pixel bias map to a file

void map_pixel_bias_save()
{
   using namespace map_pixel_bias_names;
   
   cchar    *keys[2] = { "Make", "Model" };                                      //  exif camera data
   char     *data[2];
   char     *file;
   FILE     *fid = 0;
   int      px, py, qx, qy, rx, ry, iq, ir;
   double   sumR, sumG, sumB;
   float    meanR, meanG, meanB;
   float    offsetR, offsetG, offsetB;

   if (! pixR) return;                                                           //  no data available

   if (! *bias_map_file)
   {
      exif_get(GSfiles[0],keys,data,2);                                          //  default bias measure file:
      if (! data[0]) data[0] = zstrdup("unknown");                               //    .../pixel_bias_map make model
      if (! data[1]) data[1] = zstrdup("unknown");
      snprintf(bias_map_file,200,"%s/pixel_bias_map %s %s",
                              pixel_maps_folder, data[0], data[1]);
   }

   file = zgetfile(E2X("Pixel Bias Map file"),MWIN,"save",bias_map_file);        //  choose file for write
   if (! file) return;

   fid = fopen(file,"w");                                                        //  open for write
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }
   
   strncpy0(bias_map_file,file,200);                                             //  remember chosen file name

   Ffuncbusy = 1;

   fprintf(fid,"pixel_bias_map \n");                                             //  write headers
   fprintf(fid,"dimensions %d %d \n",Eww,Ehh);
   fprintf(fid,"pixel RGB offsets \n");   
   
   for (py = 2; py < Ehh-2; py++)                                                //  RGB offsets from neighbor pixel mean
   for (px = 2; px < Eww-2; px += 10)
   {
      qy = py;
      for (qx = px; qx < px+10 && qx < Eww-2; qx++) 
      {
         sumR = sumG = sumB = 0;

         for (ry = qy-2; ry <= qy+2; ry++)                                       //  RGB sum of 5x5 neighbor block
         for (rx = qx-2; rx <= qx+2; rx++)
         {
            ir = ry * Eww + rx;
            sumR += pixR[ir];
            sumG += pixG[ir];
            sumB += pixB[ir];
         }
         
         meanR = sumR / 25;                                                      //  RGB mean of 5x5 neighbor block
         meanG = sumG / 25;
         meanB = sumB / 25;

         iq = qy * Eww + qx;
         offsetR = meanR / pixR[iq];                                             //  pixel RGB offset from 5x5 block
         offsetG = meanG / pixG[iq];                                             //  e.g. 1% too bright >> 0.99
         offsetB = meanB / pixB[iq];

         fprintf(fid,"%.3f %.3f %.3f ",offsetR,offsetG,offsetB);
      }

      fprintf(fid,"\n");
      zmainloop(100);
   }

   fclose(fid);
   Ffuncbusy = 0;
   return;
}


//  Load pixel bias map from a file.
//  returns: 0 = OK    called by m_batch_RAW 

namespace pixel_bias_map_names
{
   int      Eww, Ehh;
   float    *offsetR = 0, *offsetG, *offsetB;
}   


int pixel_bias_map_load()
{
   using namespace pixel_bias_map_names;

   char     *file;
   int      nn, ii;
   int      px, py, qx, qy;
   FILE     *fid = 0;

   if (offsetR) 
   {
      zfree(offsetR);
      zfree(offsetG);
      zfree(offsetB);
      offsetR = 0;
   }
   
   file = zgetfile(E2X("Pixel Bias Map file"),MWIN,"file",pixel_maps_folder);    //  choose file
   if (! file) return 1;

   fid = fopen(file,"r");
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return 2;
   }

   nn = fscanf(fid,"pixel_bias_map ");                                           //  read headers
   nn = fscanf(fid,"dimensions %d %d ",&Eww,&Ehh);
   if (nn != 2) goto filerr;
   nn = fscanf(fid,"pixel RGB offsets ");   
   
   nn = Eww * Ehh;
   offsetR = (float *) zmalloc(nn * sizeof(float));
   offsetG = (float *) zmalloc(nn * sizeof(float));
   offsetB = (float *) zmalloc(nn * sizeof(float));
   
   Ffuncbusy = 1;
   Fbusy_goal = Eww * Ehh;
   Fbusy_done = 0;
   
   for (py = 2; py < Ehh-2; py++)                                                //  get RGB offsets from neighbor
   for (px = 2; px < Eww-2; px += 10)                                            //    pixel means
   {
      qy = py;
      for (qx = px; qx < px+10 && qx < Eww-2; qx++) 
      {
         ii = qy * Eww + qx;
         nn = fscanf(fid,"%f %f %f ",&offsetR[ii],&offsetG[ii],&offsetB[ii]);
         if (nn != 3) goto filerr;
      }
      
      Fbusy_done += 10;
      zmainloop(10);
   }

   fclose(fid);
   Ffuncbusy = 0;
   Fbusy_goal = Fbusy_done = 0;
   return 0;   

filerr:

   zmessageACK(Mwin,E2X("invalid pixel bias map file"));

   fclose(fid);

   if (offsetR) {
      zfree(offsetR);
      zfree(offsetG);
      zfree(offsetB);
      offsetR = 0;
   }

   Ffuncbusy = 0;
   Fbusy_goal = Fbusy_done = 0;
   return 3;
}


//  apply pixel bias corrections for given PXM image
//  returns: 0 = OK    called by m_batch_RAW 

int pixel_bias_fix(PXM * pxm)
{
   using namespace pixel_bias_map_names;
   
   int      px, py, ii;
   float    *pix;
   
   if (! offsetR) return 1;                                                      //  no map file loaded
   
   if (Eww != pxm->ww || Ehh != pxm->hh) {                                       //  dimensions wrong
      zmessageACK(Mwin,E2X("image dimensions do not match pixel bias file"));
      return 2;
   }

   for (py = 2; py < Ehh-2; py++)
   for (px = 2; px < Eww-2; px++)
   {
      pix = PXMpix(pxm,px,py);
      ii = py * Eww + px;
      pix[0] = pix[0] * offsetR[ii];
      pix[1] = pix[1] * offsetG[ii];
      pix[2] = pix[2] * offsetB[ii];
      if (pix[0] > 255) pix[0] = 255;
      if (pix[1] > 255) pix[1] = 255;
      if (pix[2] > 255) pix[2] = 255;
   }

   return 0;
}


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

//   Search and map RAW image dead pixels (gray image outliers) 

namespace map_dead_pixels_names
{
   zdialog  *zd;
   char     dead_pixels_file[200] = "";                                          //  dead pixel map file
   int      Eww, Ehh;                                                            //  gray image dimensions
   int      threshold = 80;                                                      //  dead pixel threshold
   char     *rawfile = 0;                                                        //  RAW file to use
   PXM      *pxm = 0;

   typedef struct {                                                              //  dead pixel table
      int      cx, cy;                                                           //  center pixel
      int      Np;                                                               //  pixel count, 0-9
      int      px[9], py[9];                                                     //  pixels in dead group
   }  deadpix_t;

   deadpix_t   deadpixtab[50];                                                   //  dead pixel table
   int         Ndead, maxdead = 50;

   int      deadreptab[9][12] =                                                  //  dead pixel replacement table
   {
//       px py     rx ry    replacement pixels
      {  -1,-1,    -2,-2,  -2,-1,  -1,-2,  9, 9,  9, 9  },                       //  px/py position in 3x3 group
      {   0,-1,     0,-2,   9, 9,   9, 9,  9, 9,  9, 9  },                       //  corresp. 1-4 replacement pixels
      {   1,-1,     1,-2,   2,-2,   2,-1,  9, 9,  9, 9  },                       //  9/9 = EOL
      {  -1, 0,    -2, 0,   9, 9,   9, 9,  9, 9,  9, 9  },
      {   0, 0,    -2, 0,   0,-2,   2, 0,  0, 2,  9, 9  },
      {   1, 0,     2, 0,   9, 9,   9, 9,  9, 9,  9, 9  },
      {  -1, 1,    -2, 1,  -2, 2,  -1, 2,  9, 9,  9, 9  },
      {   0, 1,     0, 2,   9, 9,   9, 9,  9, 9,  9, 9  },
      {   1, 1,     2, 1,   2, 2,   1, 2,  9, 9,  9, 9  }  
   };
}


//  menu function

void m_map_dead_pixels(GtkWidget *, cchar *menu)                                 //  20.0
{
   using namespace map_dead_pixels_names;

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

   F1_help_topic = "map dead pixels";
   if (checkpend("all")) return;

   m_viewmode(0,"F");
   
/***
       ___________________________________
      |       Map RAW Dead Pixels         |
      |                                   |
      | [select] gray RAW image file      |
      | [__] RGB threshold                |
      | dead pixels found: NN             |
      | RAW loader: xxxxxxx               |
      |                                   |
      |            [find] [save] [cancel] |
      |___________________________________|

***/

   zd = zdialog_new(E2X("Map RAW Dead Pixels"),Mwin,Bfind,Bsave,Bcancel,null);

   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","select","hbf",Bselect,"space=5");
   zdialog_add_widget(zd,"label","labsel","hbf",E2X("gray RAW image file"),"space=10");
   
   zdialog_add_widget(zd,"hbox","hbthresh","dialog",0,"space=5");
   zdialog_add_widget(zd,"zspin","thresh","hbthresh","10|255|1|80","space=5");
   zdialog_add_widget(zd,"label","labthresh","hbthresh",E2X("RGB threshold"),"space=5");

   zdialog_add_widget(zd,"hbox","hbdead","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labdead","hbdead",E2X("dead pixels found:"),"space=5");
   zdialog_add_widget(zd,"label","deadpixels","hbdead","no data");
   
   zdialog_add_widget(zd,"hbox","hbrload","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labrload","hbrload","RAW loader:","space=5");
   zdialog_add_widget(zd,"label","rawloader","hbrload","none","space=5");
   
   zdialog_stuff(zd,"thresh",threshold);
   if (Frawloader == 1) zdialog_stuff(zd,"rawloader","dcraw");
   if (Frawloader == 2) zdialog_stuff(zd,"rawloader","RawTherapee");

   zdialog_run(zd,map_dead_pixels_dialog_event,"parent");
   
   return;
}


//  dialog event and completion callback function

int map_dead_pixels_dialog_event(zdialog *zd, cchar *event)
{
   using namespace map_dead_pixels_names;

   void  map_dead_pixels_find(zdialog *zd);
   void  map_dead_pixels_save();

   char     *pp;
   int      err, Nth = 0;

   if (strmatch(event,"select"))                                                 //  select RAW image
   {
      if (rawfile) zfree(rawfile);

      rawfile = gallery_select1(null);
      if (! rawfile) return 1;

      if (image_file_type(rawfile) != RAW) {
         zmessageACK(Mwin,E2X("not a RAW file"));
         return 1;
      }

      pxm = RAW_PXM_load(rawfile,0,0);
      if (! pxm) {
         zmessageACK(Mwin,E2X("cannot load RAW file"));
         return 1;
      }

      err = f_open(rawfile,Nth,0,1,0);
      if (err) return 1;

      pp = strrchr(rawfile,'/');
      if (pp) zdialog_stuff(zd,"labsel",pp+1);
   }

   if (strmatch(event,"thresh"))                                                 //  get RGB threshold
      zdialog_fetch(zd,"thresh",threshold);

   if (zd->zstat)
   {
      if (zd->zstat == 1)                                                        //  [find]
      {
         zd->zstat = 0;
         map_dead_pixels_find(zd);
         return 1;
      }
      
      else if (zd->zstat == 2)                                                   //  [save]
      {
         zd->zstat = 0;
         map_dead_pixels_save();
         return 1;
      }
      
      else                                                                       //  [cancel] or [x]
      {
         zdialog_free(zd);
         if (rawfile) zfree(rawfile);
         rawfile = 0;
         if (pxm) PXM_free(pxm);
         pxm = 0;
         Ndead = 0;
         return 1;
      }
   }
   
   return 1;
}


//  map dead pixels (high contrast with gray background)

void map_dead_pixels_find(zdialog *zd)
{
   using namespace map_dead_pixels_names;

   static zdialog  *zd2 = 0;

   int      ii, jj;
   int      px, py, qx, qy, rx, ry;
   float    R, G, B;
   float    match, contrast, maxcon;
   float    *pix1;
   char     text[20];


   if (! pxm) {
      zmessageACK(Mwin,E2X("choose a gray RAW file"));
      return;
   }
   
   if (zd2) popup_report_close(zd2,0);
   zd2 = popup_report_open("Dead Pixels",Mwin,600,300,0,0,0);
   
   Eww = pxm->ww;
   Ehh = pxm->hh;
   
   Ndead = 0;
   
   for (py = 5; py < Ehh-5; py++)                                                //  loop image pixels
   for (px = 5; px < Eww-5; px++)                                                //  (omit edges)
   {
      R = G = B = 0;

      for (qy = py-3; qy <= py+3; qy++)                                          //  loop 7x7 neighborhood
      for (qx = px-3; qx <= px+3; qx++)
      {
         pix1 = PXMpix(pxm,qx,qy);
         R += pix1[0];                                                           //  get pixel RGB sums
         G += pix1[1];
         B += pix1[2];
      }

      R = 0.0204 * R;                                                            //  RGB sums/49 = RGB means
      G = 0.0204 * G;
      B = 0.0204 * B;
      
      pix1 = PXMpix(pxm,px,py);
      match = RGBMATCH(pix1[0],pix1[1],pix1[2],R,G,B);                           //  compare pixel to 7x7 block
      contrast = 1 - match;
      if (contrast < 0.004 * threshold) continue;                                //  not a dead pixel candidate

      maxcon = -1;
      rx = ry = 0;

      for (qy = py-1; qy <= py+1; qy++)                                          //  loop 3x3 block around candidate
      for (qx = px-1; qx <= px+1; qx++)
      {
         pix1 = PXMpix(pxm,qx,qy);                                               //  find pixel with highest contrast
         match = RGBMATCH(pix1[0],pix1[1],pix1[2],R,G,B);                        //  will be center of dead group
         contrast = 1 - match;
         if (contrast > maxcon) {
            maxcon = contrast;
            rx = qx;
            ry = qy;
         }
      }
      
      qx = rx;                                                                   //  qx/qy = center of dead group
      qy = ry;
      
      for (ii = 0; ii < Ndead; ii++)                                             //  search dead pixel table
      for (jj = 0; jj < deadpixtab[ii].Np; jj++)                                 //    to see if qx/qy included
         if (qx == deadpixtab[ii].px[jj] && 
             qy == deadpixtab[ii].py[jj]) break;

      if (ii < Ndead) continue;                                                  //  yes, ignore qx/qy

      if (Ndead == maxdead) break;                                               //  add to dead pixels table
      ii = Ndead++;
      deadpixtab[ii].cx = qx;
      deadpixtab[ii].cy = qy;
      jj = 0;

      for (ry = qy-1; ry <= qy+1; ry++)                                          //  loop 3x3 block around qx/qy
      for (rx = qx-1; rx <= qx+1; rx++)
      {
         pix1 = PXMpix(pxm,rx,ry);                                               //  test contrast of pixel
         match = RGBMATCH(pix1[0],pix1[1],pix1[2],R,G,B);
         contrast = 1 - match;
         if (contrast < 0.5 * maxcon) continue;                                  //  low, omit from dead group
         deadpixtab[ii].px[jj] = rx;                                             //  high, include in dead group
         deadpixtab[ii].py[jj] = ry;
         jj++;
      }

      deadpixtab[ii].Np = jj;                                                    //  final pixel count

      popup_report_write(zd2,0,"dead pixel group: %d %d ",qx,qy);
      for (jj = 0; jj < deadpixtab[ii].Np; jj++)
      popup_report_write(zd2,0,"%d %d ", deadpixtab[ii].px[jj],
                                         deadpixtab[ii].py[jj] );
      popup_report_write(zd2,0,"\n");

      px = qx + 2;                                                               //  continue search 
   }
   
   erase_topcircles();                                                           //  erase prior circles
   
   for (ii = 0; ii < Ndead; ii++)                                                //  write circles around dead pixels
   {
      px = deadpixtab[ii].cx;
      py = deadpixtab[ii].cy;
      add_topcircle(px,py,20);
   }

   snprintf(text,20,"%d",Ndead);                                                 //  update dead pixel count in dialog
   if (Ndead == maxdead) strcat(text,"+");
   zdialog_stuff(zd,"deadpixels",text);

   Fpaint2();                                                                    //  update window
   return;
}


//  save pixel map data to a file

void map_dead_pixels_save()
{
   using namespace map_dead_pixels_names;
   
   cchar    *keys[2] = { "Make", "Model" };                                      //  exif camera data
   char     *data[2];
   char     *file;
   FILE     *fid = 0;
   int      ii, jj;
   
   if (! Ndead) return;

   if (! *dead_pixels_file)
   {
      exif_get(rawfile,keys,data,2);                                             //  dead pixels RAW file:
      if (! data[0]) data[0] = zstrdup("unknown");                               //    .../dead_pixels make model
      if (! data[1]) data[1] = zstrdup("unknown");
      snprintf(dead_pixels_file,200,"%s/dead_pixels %s %s",
                              pixel_maps_folder, data[0], data[1]);
   }

   file = zgetfile(E2X("dead pixels file"),MWIN,"save",dead_pixels_file);        //  choose file for write
   if (! file) return;

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

   fprintf(fid,"dead pixels \n");                                                //  write headers
   fprintf(fid,"dimensions %d %d \n",Eww,Ehh);
   fprintf(fid,"dead pixel groups \n");

   for (ii = 0; ii < Ndead; ii++)                                                //  write dead pixels table
   {
      fprintf(fid,"center: %d %d ", deadpixtab[ii].cx,
                                    deadpixtab[ii].cy);
      fprintf(fid,"  Np: %d ",deadpixtab[ii].Np);                                //  Np px py px py ... \n
      for (jj = 0; jj < deadpixtab[ii].Np; jj++)
         fprintf(fid," %5d %5d ", deadpixtab[ii].px[jj], 
                                  deadpixtab[ii].py[jj] );
      fprintf(fid,"\n");
   }

   fclose(fid);

   return;
}


//  Load dead pixel list from previously saved file.
//  returns: 0 = OK    Called by m_batch_RAW()

int dead_pixels_map_load()
{
   using namespace map_dead_pixels_names;

   char     *file = 0;
   FILE     *fid = 0;
   int      ii, jj, nn, Np;
   int      cx, cy, px, py;

   file = zgetfile(E2X("dead pixels file"),MWIN,"file",pixel_maps_folder);       //  choose file
   if (! file) return 1;

   fid = fopen(file,"r");                                                        //  open file
   if (! fid) {
      zmessageACK(Mwin,Bfilenotfound);
      zfree(file);
      return 2;
   }

   zfree(file);

   nn = fscanf(fid,"dead pixels ");                                              //  read header
   if (nn != 0) goto filerr;
   
   nn = fscanf(fid,"dimensions %d %d ",&Eww,&Ehh);                               //  image dimensions
   if (nn != 2) goto filerr;

   nn = fscanf(fid,"dead pixel groups ");                                        //  read header
   if (nn != 0) goto filerr;

   for (ii = 0; ii < maxdead; ii++)
   {
      nn = fscanf(fid,"center: %d %d ",&cx,&cy);                                 //  read central pixel
      if (nn == EOF) break;
      if (nn != 2) goto filerr;

      deadpixtab[ii].cx = cx;
      deadpixtab[ii].cy = cy;

      nn = fscanf(fid,"Np: %d ",&Np);                                            //  read pixel count
      if (nn != 1) goto filerr;

      deadpixtab[ii].Np = Np;
      
      for (jj = 0; jj < Np; jj++)
      {
         nn = fscanf(fid,"%d %d ",&px,&py);                                      //  read pixel group members
         if (nn != 2) goto filerr;

         deadpixtab[ii].px[jj] = px;
         deadpixtab[ii].py[jj] = py;
      }
   }

   Ndead = ii;                                                                   //  final pixel count
   fclose(fid);
   fid = 0;

   if (Ndead > 0 && nn == EOF) return 0;

filerr:
   zmessageACK(Mwin,E2X("invalid dead pixels file"));
   if (fid) fclose(fid);
   return 3;
}


//  Fix all dead pixels in deadpixtab for given image in memory.
//  returns: 0 = OK   Called by m_batch_RAW()

int dead_pixels_fix(PXM *pxm)
{
   using namespace map_dead_pixels_names;

   int      ii, jj, kk, mm;
   int      Np, Nr;
   int      cx, cy, px, py, rx, ry;
   float    R, G, B;
   float    *pix1;
   
   if (! Ndead) {
      zmessageACK(Mwin,E2X("no dead pixels data available"));
      return 1;
   }
   
   if (Eww != pxm->ww || Ehh != pxm->hh) {
      zmessageACK(Mwin,E2X("image dimensions do not match dead pixels file"));
      return 2;
   }
   
   for (ii = 0; ii < Ndead; ii++)                                                //  loop dead pixels table
   {
      cx = deadpixtab[ii].cx;                                                    //  center pixel of dead pixel group
      cy = deadpixtab[ii].cy;

      Np = deadpixtab[ii].Np;                                                    //  dead group pixel count

      for (jj = 0; jj < Np; jj++)                                                //  loop dead group pixels
      {
         px = deadpixtab[ii].px[jj];
         py = deadpixtab[ii].py[jj];

         for (kk = 0; kk < 9; kk++)                                              //  find row in dead pixel
         {                                                                       //    replacement table
            if (px-cx != deadreptab[kk][0]) continue;
            if (py-cy == deadreptab[kk][1]) break;
         }
         
         if (kk == 9) {
            printz("no dead pixel replacement: %d %d %d %d \n",cx,cy,px,py);
            return 3;
         }
         
         R = G = B = 0;                                                          //  sum RGB for replacement pixels
         Nr = 0;
         
         for (mm = 2; mm < 10; mm += 2)                                          //  loop replacement pixels rx/ry
         {
            rx = deadreptab[kk][mm];                                             //  pixel, relative to cx/cy
            ry = deadreptab[kk][mm+1];
            if (rx == 9) break;                                                  //  EOL

            rx = rx + cx;                                                        //  relative to absolute
            ry = ry + cy;

            pix1 = PXMpix(pxm,rx,ry);                                            //  accumulate RGB sum
            R += pix1[0];
            G += pix1[1];
            B += pix1[2];
            Nr++;
         }
         
         pix1 = PXMpix(pxm,px,py);                                               //  replace dead pixel with mean
         pix1[0] = R / Nr;                                                       //    RGB of replacement pixels
         pix1[1] = G / Nr;
         pix1[2] = B / Nr;
      }      
   }

   return 0;
}


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

//  monitor color and contrast test function

void m_monitor_color(GtkWidget *, cchar *)
{
   char        file[200];
   int         err;
   char        *savecurrfile = 0;
   char        *savegallery = 0;
   zdialog     *zd;
   cchar       *message = E2X("Brightness should show a gradual ramp \n"
                              "extending all the way to the edges.");

   F1_help_topic = "monitor color";
   m_viewmode(0,"F");                                                            //  file view mode                     19.0

   if (checkpend("all")) return;                                                 //  check nothing pending
   Fblock = 1;
   
   if (curr_file) savecurrfile = zstrdup(curr_file);                             //  20.0
   if (navi::galleryname) savegallery = zstrdup(navi::galleryname);

   snprintf(file,200,"%s/moncolor.png",get_zimagedir());                         //  color chart file

   err = f_open(file,0,0,1);
   if (err) goto restore;

   Fzoom = 1;
   gtk_window_set_title(MWIN,"check monitor");

   zd = zdialog_new("check monitor",Mwin,Bdone,null);                            //  start user dialog
   if (message) {
      zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
      zdialog_add_widget(zd,"label","lab1","hb1",message,"space=5");
   }

   zdialog_resize(zd,300,0);
   zdialog_set_modal(zd);
   zdialog_run(zd,0,"0/0");
   zdialog_wait(zd);                                                             //  wait for dialog complete
   zdialog_free(zd);

restore:

   Fzoom = 0;

   if (savecurrfile) {                                                           //  20.0
      f_open(savecurrfile);
      zfree(savecurrfile);
   }

   if (savegallery) {
      gallery(savegallery,"init",0);
      gallery(0,"sort",-2);                                                      //  recall sort and position
      zfree(savegallery);
   }
   else gallery(topfolders[0],"init",0);
   
   Fblock = 0;
   return;
}


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

//  check and adjust monitor gamma

void m_monitor_gamma(GtkWidget *, cchar *)
{
   int   mongamma_dialog_event(zdialog *zd, cchar *event);

   int         err;
   char        gammachart[200];
   zdialog     *zd;
   char        *savecurrfile = 0;
   char        *savegallery = 0;

   cchar       *permit = "Chart courtesy of Norman Koren";
   cchar       *website = "http://www.normankoren.com/makingfineprints1A.html#gammachart";

   F1_help_topic = "monitor gamma";
   m_viewmode(0,"F");                                                            //  file view mode                     19.0

   if (checkpend("all")) return;                                                 //  check nothing pending

   err = shell_quiet("which xgamma");                                            //  check for xgamma
   if (err) {
      zmessageACK(Mwin,"xgamma program is not installed");
      return;
   }

   Fblock = 1;

   if (curr_file) savecurrfile = zstrdup(curr_file);                             //  20.0
   if (navi::galleryname) savegallery = zstrdup(navi::galleryname);

   snprintf(gammachart,200,"%s/mongamma.png",get_zimagedir());                   //  gamma chart file
   err = f_open(gammachart);
   if (err) goto restore;

   Fzoom = 1;                                                                    //  scale 100% (required)
   gtk_window_set_title(MWIN,"monitor gamma");

   zd = zdialog_new("monitor gamma",Mwin,Bdone,null);                            //  start user dialog
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=8");
   zdialog_add_widget(zd,"label","labgamma","hb1","gamma","space=5");
   zdialog_add_widget(zd,"hscale","gamma","hb1","0.6|1.4|0.02|1.0","expand");
   zdialog_add_widget(zd,"hbox","hb2","dialog");
   zdialog_add_widget(zd,"link",permit,"hb2",website);

   zdialog_resize(zd,200,0);
   zdialog_restore_inputs(zd);                                                   //  preload prior user inputs
   zdialog_set_modal(zd);
   zdialog_run(zd,mongamma_dialog_event,"0/0");
   zdialog_wait(zd);                                                             //  wait for dialog complete
   zdialog_free(zd);

restore:

   Fzoom = 0;

   if (savecurrfile) {                                                           //  20.0
      f_open(savecurrfile);
      zfree(savecurrfile);
   }

   if (savegallery) {
      gallery(savegallery,"init",0);
      gallery(0,"sort",-2);                                                      //  recall sort and position
      zfree(savegallery);
   }
   else gallery(topfolders[0],"init",0);

   Fblock = 0;
   return;
}


//  dialog event function

int mongamma_dialog_event(zdialog *zd, cchar *event)
{
   double   gamma;

   if (strmatch(event,"gamma")) {
      zdialog_fetch(zd,"gamma",gamma);
      shell_ack("xgamma -quiet -gamma %.2f",gamma);
   }

   return 0;
}


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

//  set GUI language

void m_change_lang(GtkWidget *, cchar *)
{
   #define NL 8

   zdialog     *zd;
   int         ii, cc, val, zstat;
   char        lang1[NL], *pp;

   cchar  *langs[NL] = { "en English", "de German",                              //  english first
                         "ca Catalan", "es Spanish",
                         "fr French", "it Italian", 
                         "pt Portuguese", null  };

   cchar  *title = E2X("Available Translations");

   F1_help_topic = "change language";

   zd = zdialog_new(E2X("Set Language"),Mwin,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"label","title","dialog",title,"space=5");
   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb1");

   for (ii = 0; langs[ii]; ii++)                                                 //  make radio button per language
      zdialog_add_widget(zd,"radio",langs[ii],"vb1",langs[ii]);

   cc = strlen(zfuncs::zlocale);                                                 //  current language
   for (ii = 0; langs[ii]; ii++)                                                 //  match on lc_RC
      if (strmatchN(zfuncs::zlocale,langs[ii],cc)) break;
   if (! langs[ii])
      for (ii = 0; langs[ii]; ii++)                                              //  failed, match on lc alone
         if (strmatchN(zfuncs::zlocale,langs[ii],2)) break;
   if (! langs[ii]) ii = 0;                                                      //  failed, default english
   zdialog_stuff(zd,langs[ii],1);

   zdialog_resize(zd,200,0);
   zdialog_set_modal(zd);
   zdialog_run(zd,0,"mouse");                                                    //  run dialog
   zstat = zdialog_wait(zd);

   for (ii = 0; langs[ii]; ii++) {                                               //  get active radio button
      zdialog_fetch(zd,langs[ii],val);
      if (val) break;
   }

   zdialog_free(zd);                                                             //  kill dialog

   if (zstat != 1) return;                                                       //  user cancel
   if (! val) return;                                                            //  no selection

   strncpy0(lang1,langs[ii],NL);
   pp = strchr(lang1,' ');                                                       //  isolate lc_RC part
   *pp = 0;

   locale = zstrdup(lang1);                                                      //  set parameter 'locale'             19.0
   save_params();
   
   new_session("-p");                                                            //  replace readlink() etc.            19.0
   zsleep(1);                                                                    //  delay before SIGTERM in quitxx()   19.0
   quitxx();                                                                     //  exit

   return;
}


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

//  report missing translations in a popup window
//  also export translation file for current locale                              20.0

void m_untranslated(GtkWidget *, cchar *)
{
   int      ftf = 1, Nmiss = 0;
   cchar    *missing;
   zdialog  *zd2;

   F1_help_topic = "missing translations";

   zd2 = popup_report_open("missing translations",Mwin,400,200,0,0,0);
   
   popup_report_write(zd2,1,"missing translations for locale %s \n",zfuncs::zlocale);

   while (true)
   {
      missing = E2X_missing(ftf);
      if (! missing) break;
      popup_report_write(zd2,0,"%s \n",missing);
      Nmiss++;
   }

   popup_report_write(zd2,0,"%d missing translations \n",Nmiss);

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

   char     tranfile[200], *pp;                                                  //  ask to export translation file     20.0
   int      yn, err;                                                             //  (trapped in appimage container)
   STATB    statB;

   yn = zmessageYN(Mwin,"export the translation .po file?");
   if (! yn) return;

   locale_filespec("locale","translate.po",tranfile);
   printz("translation file: %s \n",tranfile);

   err = stat(tranfile,&statB);
   if (err) {
      zmessageACK(Mwin,"file not found");
      return;
   }

   shell_ack("cp %s %s",tranfile,get_zhomedir());                                //  copy to fotoxx home folder
   pp = strrchr(tranfile,'/');
   zmessageACK(Mwin,"created: %s/%s",get_zhomedir(),pp+1);

   return;
}


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

//  user can allow or block anonymous usage statistics sent to web host

void m_phone_home_allow(GtkWidget *, cchar *)                                    //  20.0
{
   phone_home_allow(Mwin);
   return;
}


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

//  show CPU time since last time shown

void m_resources(GtkWidget *, cchar *)                                           //  20.0
{
   static double  time0 = 0.0;
   double         time1;
   double         mem = 0;
   char           buff1[100], buff2[1000];
   char           *pp = 0;
   FILE           *fid;
   int            MB, pagesize;
   int            mega = 1024 * 1024;
   
   F1_help_topic = "show resources";
   
   time1 = CPUtime();
   printz("CPU time: %.3f seconds \n", time1 - time0);
   time0 = time1;

   snprintf(buff1,100,"/proc/self/stat");                                        //  read file /proc/self/stat
   fid = fopen(buff1,"r");
   if (fid) {
      pp = fgets(buff2,1000,fid);
      fclose(fid);
   }
   
   if (pp) {
      pp = strchr(pp,')');                                                       //  closing ')' after (short) filename
      if (pp) {
         parseprocrec(pp+1,22,&mem,null);                                        //  get real memory
         pagesize = sysconf(_SC_PAGESIZE);                                       //  system page size
         MB = mem * pagesize / mega;
         printz("real memory MB: %d \n",MB);
      }
   }

   return;
}

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

//  list files included in the appimage container
//  .../fotoxx/*  files are excluded

void m_appimage_files(GtkWidget *, cchar *)                                      //  20.0
{
   zdialog  *zd;
   FILE     *fid;
   char     buff[100];
   cchar    *HERE, *pp;
   cchar    *command = "echo HERE: $HERE; find $HERE/usr/bin; "
                       "find $HERE/usr/lib; find $HERE/usr/share";

   F1_help_topic = "appimage files";
   
   if (! zfuncs::appimagexe) {
      zmessageACK(Mwin,"not an appimage build");
      return;
   }
   
   zd = popup_report_open("appimage container included files",Mwin,500,500,0,0,0);

   HERE = getenv("HERE");
   popup_report_write(zd,0,"HERE: %s\n",HERE);
   
   fid = popen(command,"r");
   if (! fid) return;

   while (true) {
      pp = fgets_trim(buff,100,fid);
      if (! pp) break;
      if (strstr(pp,"/fotoxx")) continue;
      pp = strstr(pp,HERE);
      if (! pp) continue;
      pp += strlen(HERE);
      popup_report_write(zd,0,"%s\n",pp);
   }
   
   pclose(fid);
   popup_report_top(zd);

   return;
}


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

//  zappcrash test - make a segment fault

void m_zappcrash_test(GtkWidget *, cchar *)                                      //  19.0
{
   printz("zappcrash from menu Zappcrash Test \n");
   zappcrash("zappcrash from menu Zappcrash Test");
   return;
}



