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

   Fotocx - edit photos and manage collections

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

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

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

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

   Fotocx image edit - Batch Menu functions

   m_batch_convert         batch rename, convert, rescale, move
   m_batch_copy_move       batch copy/move selected files to another folder
   m_batch_upright         find rotated files and upright them
   m_batch_deltrash        delete or trash selected files
   m_batch_overlay         add overlay image (copyright ...) to selected files
   m_export_filelist       select files and generate a file list (text)
   m_export_files          select files and export to a folder

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

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

using namespace zfuncs;

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

//  Batch file rename, convert, rescale, move

namespace batch_convert_names
{
   int      Fsametype, Fsamebpc, newbpc, jpeg_quality;
   int      Fupsize, Fdownsize, maxww, maxhh;
   int      Fdelete, Fcopymeta, Fupalbums, Freplace, Fnewtext;
   int      Fplugdate, Fplugseq, Fplugname;
   int      Frescale;                                                                  //  a rescale button is pressed           25.1
   ch       Arescale[8];                                                               //  which one: "1/4" "1/3" etc.           25.1
   ch       text1[100], text2[100];
   ch       newloc[XFCC], newname[200], newext[8];
   int      baseseq, addseq;
   int      NT = 8;                                                                    //  threads
   ch       **infile, **outfile, **statmess;
   zdialog  *zdpop;
};


//  menu function

void m_batch_convert(GtkWidget *, ch *)                                                //  use parallel threads                  25.1
{
   using namespace batch_convert_names;

   int  batch_convert_dialog_event(zdialog *zd, ch *event);
   void * batch_convert_wthread(void *);

   zdialog     *zd;
   int         zstat;
   ch          text[100];
   int         im, Nrep;

   F1_help_topic = TX("BATCH CONVERT");

   printf("m_batch_convert \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

/***
       ___________________________________________________________________________
      |                          Batch Convert                                    |
      |                                                                           |
      |  [Select Files]  N files selected                                         |
      |                                                                           |
      |  Replace Text  [________________________] --> [_________________________] |       replace substring in file names
      |  New Name [_____________________________________________________________] |       $yyyy $mm $dd $oldname $ss
      |  Sequence Numbers   base [______]  adder [______]                         |
      |  New Location [_______________________________________________] [browse]  |
      |                                                                           |
      |  New Type: (o) tif  (o) png  (o) jxl  (o) jpg [90] quality  (o) no change |        jxl
      |  Color Depth: (o) 8-bit  (o) 16-bit  (o) no change                        |
      |  Max. Width [_____]  Height [_____]  [x] downsize  [x] upsize             |
      |  Rescale: (o) 1/4  (o) 1/3  (o) 1/2  (o) 2/3  (o) 1.0  (o) 1.4  (o) 2.0   |        25.1
      |  [x] Delete Originals  [x] Copy Metadata  [x] Update Albums               |
      |                                                                           |
      |                                                             [Proceed] [X] |
      |___________________________________________________________________________|

***/

   zd = zdialog_new("Batch Convert",Mwin,TX("Proceed"),"X",null);

   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","files","hbf",TX("Select Files"),"space=5");
   zdialog_add_widget(zd,"label","fcount","hbf",TX("no files selected"),"space=10");

   zdialog_add_widget(zd,"hbox","hbrep","dialog");
   zdialog_add_widget(zd,"label","labrep1","hbrep",TX("Replace Text"),"space=5");
   zdialog_add_widget(zd,"zentry","text1","hbrep",0,"expand");
   zdialog_add_widget(zd,"label","arrow","hbrep"," --> ","space=5");
   zdialog_add_widget(zd,"zentry","text2","hbrep",0,"expand");
   zdialog_add_ttip(zd,"labrep1",TX("replace substring within file name"));

   zdialog_add_widget(zd,"hbox","hbname","dialog");
   zdialog_add_widget(zd,"label","labname","hbname",TX("New Name"),"space=5");
   zdialog_add_widget(zd,"zentry","newname","hbname",0,"expand");
   zdialog_add_ttip(zd,"labname",TX("plugins: (year month day old-name sequence) $yyyy $mm $dd $oldname $s"));

   zdialog_add_widget(zd,"hbox","hbseq","dialog");
   zdialog_add_widget(zd,"label","labseq","hbseq",TX("Sequence Numbers"),"space=5");
   zdialog_add_widget(zd,"label","space","hbseq",0,"space=8");
   zdialog_add_widget(zd,"label","labbase","hbseq",TX("base"),"space=5");
   zdialog_add_widget(zd,"zspin","baseseq","hbseq","0|100000|1|0","size=5");
   zdialog_add_widget(zd,"label","space","hbseq","","space=3");
   zdialog_add_widget(zd,"label","labadder","hbseq",TX("adder"),"space=5");
   zdialog_add_widget(zd,"zspin","addseq","hbseq","0|100|1|0","size=3");
   zdialog_add_widget(zd,"label","space","hbseq",0,"space=60");                        //  push back oversized entries

   zdialog_add_widget(zd,"hbox","hbloc","dialog");
   zdialog_add_widget(zd,"label","labloc","hbloc",TX("New Location"),"space=5");
   zdialog_add_widget(zd,"zentry","newloc","hbloc",0,"expand|space=5");
   zdialog_add_widget(zd,"button","browse","hbloc",TX("Browse"),"space=5");

   zdialog_add_widget(zd,"hbox","hbft","dialog");
   zdialog_add_widget(zd,"label","labtyp","hbft",TX("New Type:"),"space=5");
   zdialog_add_widget(zd,"radio","tif","hbft","tif","space=4");
   zdialog_add_widget(zd,"radio","png","hbft","png","space=4");
   zdialog_add_widget(zd,"radio","jxl","hbft","jxl","space=4");                        //  jxl
   zdialog_add_widget(zd,"radio","jpg","hbft","jpg","space=4");
   zdialog_add_widget(zd,"zspin","jpgqual","hbft","10|100|1|90","size=3");
   zdialog_add_widget(zd,"label","labqual","hbft",TX("quality"),"space=6");
   zdialog_add_widget(zd,"radio","sametype","hbft",TX("no change"),"space=6");

   zdialog_add_widget(zd,"hbox","hbcd","dialog");
   zdialog_add_widget(zd,"label","labcd","hbcd",TX("Color Depth:"),"space=5");
   zdialog_add_widget(zd,"radio","8-bit","hbcd","8-bit","space=4");
   zdialog_add_widget(zd,"radio","16-bit","hbcd","16-bit","space=4");
   zdialog_add_widget(zd,"radio","samebpc","hbcd",TX("no change"),"space=4");

   zdialog_add_widget(zd,"hbox","hbwh","dialog");
   zdialog_add_widget(zd,"label","labw","hbwh",TX("Max. Width"),"space=5");
   zdialog_add_widget(zd,"zspin","maxww","hbwh","0|10000|1|1000","size=5");
   zdialog_add_widget(zd,"label","space","hbwh",0,"space=5");
   zdialog_add_widget(zd,"label","labh","hbwh",TX("Height"),"space=5");
   zdialog_add_widget(zd,"zspin","maxhh","hbwh","0|10000|1|700","size=5");
   zdialog_add_widget(zd,"check","downsize","hbwh",TX("downsize"),"space=20");
   zdialog_add_widget(zd,"check","upsize","hbwh",TX("upsize"));
   zdialog_add_widget(zd,"label","space","hbwh",0,"space=30");                         //  push back oversized entries
   zdialog_add_ttip(zd,"downsize",TX("reduce to these limits if larger"));
   zdialog_add_ttip(zd,"upsize",TX("expand to these limits if smaller"));

   zdialog_add_widget(zd,"hbox","hbrs","dialog");                                      //  25.1
   zdialog_add_widget(zd,"label","labrs","hbrs",TX("Rescale:"),"space=5");
   zdialog_add_widget(zd,"radio","rs-1/4","hbrs","1/4","space = 5");
   zdialog_add_widget(zd,"radio","rs-1/3","hbrs","1/3","space = 5");
   zdialog_add_widget(zd,"radio","rs-1/2","hbrs","1/2","space = 5");
   zdialog_add_widget(zd,"radio","rs-2/3","hbrs","2/3","space = 5");
   zdialog_add_widget(zd,"radio","rs-1.0","hbrs","1.0","space = 5");
   zdialog_add_widget(zd,"radio","rs-1.4","hbrs","1.4","space = 5");
   zdialog_add_widget(zd,"radio","rs-2.0","hbrs","2.0","space = 5");

   zdialog_add_widget(zd,"hbox","hbopts","dialog");
   zdialog_add_widget(zd,"check","delete","hbopts",TX("Delete Originals"),"space=3");
   zdialog_add_widget(zd,"check","copymeta","hbopts",TX("Copy Metadata"),"space=5");
   zdialog_add_widget(zd,"check","upalbums","hbopts",TX("Update Albums"),"space=5");   //  25.1

   snprintf(text,100,TX("%d image files selected"),SFcount);                           //  show selected files count
   zdialog_stuff(zd,"fcount",text);

   //  set default inputs

   zdialog_stuff(zd,"tif",0);
   zdialog_stuff(zd,"png",0);
   zdialog_stuff(zd,"jxl",0);                                                          //  jxl
   zdialog_stuff(zd,"jpg",0);
   zdialog_stuff(zd,"jpgqual",jpeg_def_quality);
   zdialog_stuff(zd,"sametype",1);                                                     //  same file type

   zdialog_stuff(zd,"8-bit",0);
   zdialog_stuff(zd,"16-bit",0);
   zdialog_stuff(zd,"samebpc",1);                                                      //  same bits/color

   *newloc = 0;                                                                        //  no new location

   zdialog_stuff(zd,"downsize",0);                                                     //  downsize - no
   zdialog_stuff(zd,"upsize",0);                                                       //  upsize - no

   zdialog_stuff(zd,"rs-1/4",0);                                                       //  25.1
   zdialog_stuff(zd,"rs-1/3",0);
   zdialog_stuff(zd,"rs-1/2",0);
   zdialog_stuff(zd,"rs-2/3",0);
   zdialog_stuff(zd,"rs-1.0",1);                                                       //  no rescale
   zdialog_stuff(zd,"rs-1.4",0);
   zdialog_stuff(zd,"rs-2.0",0);

   zdialog_stuff(zd,"delete",0);                                                       //  delete originals - no
   zdialog_stuff(zd,"copymeta",1);                                                     //  copy metadata - yes
   zdialog_stuff(zd,"upalbums",0);                                                     //  update albums - no                    25.1

   zdialog_load_inputs(zd);                                                            //  preload prior user inputs

   //  run dialog, get user inputs

   zdialog_run(zd,batch_convert_dialog_event,"parent");                                //  run dialog
   zstat = zdialog_wait(zd);                                                           //  wait for completion
   zdialog_free(zd);

   if (zstat != 1) return;                                                             //  canceled
   if (! SFcount) return;                                                              //  no input files

   if (Fblock("batch_convert")) return;                                                //  block other functions

   //  start processing image files

   zadd_locked(NFbusy,+1);                                                             //  top panel busy indicator

   infile = (ch **) zmalloc(SFcount * sizeof(ch **),"batch_convert");                  //  make table of file names
   outfile = (ch **) zmalloc(SFcount * sizeof(ch **),"batch_convert");
   statmess = (ch **) zmalloc(SFcount * sizeof(ch **),"batch_convert");

   for (im = 0; im < SFcount; im++) {
      infile[im] = zstrdup(SelFiles[im],"batch_convert");                              //  input file name
      outfile[im] = 0;                                                                 //  output file name
      statmess[im] = TX("waiting");                                                    //  conversion status
   }

   zdpop = popup_report_open(TX("converting files"),Mwin,600,300,0,0,0,"X",null);      //  open popup report
   zmainloop();

   for (im = 0; im < NT; im++)                                                         //  start threads for conversion work
      start_detached_thread(batch_convert_wthread,&Nval[im]);

   for (Nrep = 0; Nrep < SFcount;)
   {
      for (im = 0; im < SFcount; im++)                                                 //  report results
      {
         if (statmess[im] && ! strmatch(statmess[im],TX("waiting"))) {                 //  look for next finished file
            popup_report_write2(zdpop,0,"\n");
            popup_report_write2(zdpop,0,TX("input file: %s \n"),infile[im]);           //  log input file
            popup_report_write2(zdpop,0,TX("output file: %s \n"),outfile[im]);         //  log output file
            if (strmatch(statmess[im],TX("completed")))
               popup_report_write2(zdpop,0,TX("status: completed \n"));                //  report status
            else popup_report_write2(zdpop,1,TX("status: %s \n"),statmess[im]);        //  bold if not "completed"
            statmess[im] = 0;
            Nrep++;
            zmainloop();
         }
      }
      zmainsleep(0.1);
   }

   //  processing complete

   if (! zdialog_valid(zdpop))
      popup_report_write2(zdpop,0,TX("\n *** CANCELED \n"));
   else
      popup_report_write2(zdpop,0,TX("\n *** COMPLETED \n"));

   popup_report_bottom(zdpop);

   zadd_locked(NFbusy,-1);

   index_rebuild(2,0);                                                                 //  update index and thumbnails           25.1

   if (Fupalbums)
      album_purge_replace("ALL",SFcount,infile,outfile);                               //  update album files                    25.1

   Fblock(0);

   for (im = 0; im < SFcount; im++) {                                                  //  free memory
      zfree(infile[im]);
      if (outfile[im]) zfree(outfile[im]);
   }

   zfree(infile);
   zfree(outfile);
   zfree(statmess);

   if (*newloc) gallery(newloc,"init",0);                                              //  refresh gallery                       25.1
   else gallery(0,"init",0);
   gallery(0,"sort",-2);                                                               //  recall sort and position
   gallery(0,"paint",-1);                                                              //  repaint from same position
   viewmode('G');

   return;
}


//  thread function which does the conversion work without use of GTK functions

void * batch_convert_wthread(void *arg)
{
   using namespace batch_convert_names;

   int      index = *((int *) arg);
   int      im, jj, cc, err;
   ch       *pp;
   ch       *inloc, *inname, *inext;
   ch       *outloc, *outname, *outext;
   ch       *tempname, seqplug[8], seqnum[8];
   ch       plugyyyy[8], plugmm[4], plugdd[4];
   int      outww, outhh, outbpc, Fdelete2;
   float    scale, wscale, hscale;
   PXM      *pxmin, *pxmout;
   xxrec_t  *xxrec;

   for (im = index; im < SFcount; im += NT)                                            //  thread count
   {
      if (! zdialog_valid(zdpop)) break;                                               //  report canceled

      if ((image_file_type(infile[im]) != IMAGE) &&
          (image_file_type(infile[im]) != RAW)) {                                      //  RAW added        25.1
         statmess[im] = TX("not an image file");
         continue;
      }

      parsefile(infile[im],inloc,inname,inext);                                        //  parse folder, filename, .ext

      if (! inloc || ! inname || ! inext) {
         statmess[im] = TX("cannot parse file");
         continue;
      }

      outloc = zstrdup(inloc,"batch convert");                                         //  initial output = input file
      outname = zstrdup(inname,"batch convert");
      outext = zstrdup(inext,"batch convert",4);

      if (*newname) {                                                                  //  new name was specified
         zfree(outname);
         outname = zstrdup(newname,"batch convert");                                   //  may contain $-plugins
      }

      if (Fnewtext) {                                                                  //  file name replacement text
         tempname = zstrdup(outname,"batch convert",100);
         cc = strlen(outname) + 100;
         repl_1str(outname,tempname,cc,text1,text2);                                   //  replace text1 (if present) with text2
         zfree(outname);
         outname = tempname;
      }

      if (Fplugname)                                                                   //  insert old file name
      {
         cc = strlen(outname) + strlen(inname);                                        //  25.1
         tempname = zstrdup(outname,"batch convert",cc);
         repl_1str(outname,tempname,cc,"$oldname",inname);                             // ...$oldname...  >>  ...inname...
         zfree(outname);
         outname = tempname;
      }

      if (Fplugdate)                                                                   //  insert meta date
      {
         xxrec = get_xxrec(infile[im]);                                                //  get photo date, yyyymmddhhmmdd
         if (! *xxrec->pdate) {
            statmess[im] = TX("no photo date, skipped");
            zfree(outloc);
            zfree(outname);
            zfree(outext);
            continue;
         }

         strncpy0(plugyyyy,xxrec->pdate,5);                                            //  yyyy:mm:dd
         strncpy0(plugmm,xxrec->pdate+5,3);
         strncpy0(plugdd,xxrec->pdate+8,3);
         tempname = zstrdup(outname,"batch convert",8);
         cc = strlen(outname) + 8;
         repl_Nstrs(outname,tempname,cc,"$yyyy",plugyyyy,"$mm",plugmm,"$dd",plugdd,null);
         zfree(outname);
         outname = tempname;
      }

      if (Fplugseq)                                                                    //  insert sequence number
      {
         pp = strcasestr(outname,"$s");                                                //  find $s... in output name
         if (pp) {
            for (cc = 1; pp[cc] == pp[1] && cc < 6; cc++);                             //  length of "$s...s"  2-6 chars.
            strncpy0(seqplug,pp,cc+1);
            jj = baseseq + im * addseq;                                                //  new sequence number
            snprintf(seqnum,8,"%0*d",cc-1,jj);                                         //  1-5 chars.
            tempname = zstrdup(outname,"batch convert",8);
            cc = strlen(outname) + 8;
            repl_1str(outname,tempname,cc,seqplug,seqnum);                             //  ...$ssss...  >>  ...1234...
            zfree(outname);
            outname = tempname;
         }
      }

      if (*newloc) {                                                                   //  new location was given
         zfree(outloc);
         outloc = zstrdup(newloc,"batch convert");
      }

      if (Fsametype) {                                                                 //  new .ext = old .ext
         if (strcasestr(".jpg .jpeg .png .jxl .tif .tiff",inext))                      //  jxl
            strcpy(outext,inext);
         else strcpy(outext,".jpg");                                                   //  other >> .jpg
      }
      else strcpy(outext,newext);                                                      //  new .ext was given

      cc = strlen(outloc) + strlen(outname) + strlen(outext) + 4;                      //  construct output file name
      outfile[im] = (ch *) zmalloc(cc,"batch convert");
      snprintf(outfile[im],cc,"%s/%s%s",outloc,outname,outext);

      zfree(outloc);
      zfree(outname);
      zfree(outext);

      pxmin = PXM_load(infile[im],0);                                                  //  read input file
      if (! pxmin) {
         statmess[im] = TX("cannot load input file");
         continue;
      }

      if (*newloc) {
         if (regfile(outfile[im])) {                                                   //  check if file exists in new location
            statmess[im] = TX("output file exists");
            continue;
         }
      }

      outbpc = f_load_bpc;                                                             //  input file bits/color
      outww = pxmin->ww;                                                               //  input file size
      outhh = pxmin->hh;

      if (! Fsamebpc) outbpc = newbpc;                                                 //  new bits/color
      pp = strrchr(outfile[im],'.');
      if (pp && strmatch(pp,".jpg")) outbpc = 8;                                       //  if jpeg, force 8 bits/color

      if (Fdownsize)                                                                   //  downsize larger images
      {
         wscale = hscale = 1.0;
         if (outww > maxww) wscale = 1.0 * maxww / outww;                              //  compute new size
         if (outhh > maxhh) hscale = 1.0 * maxhh / outhh;
         if (wscale < hscale) scale = wscale;                                          //  scale < 1
         else scale = hscale;
         if (scale < 1.0) {
            outww = outww * scale + 0.5;                                               //  round
            outhh = outhh * scale + 0.5;
            pxmout = PXM_rescale(pxmin,outww,outhh);                                   //  rescaled output file
            PXM_free(pxmin);
            pxmin = 0;
         }
      }

      if (Fupsize && pxmin)                                                            //  upsize smaller images
      {
         wscale = hscale = 1.0;
         if (outww < maxww) wscale = 1.0 * maxww / outww;                              //  compute new size
         if (outhh < maxhh) hscale = 1.0 * maxhh / outhh;
         if (wscale < hscale) scale = wscale;                                          //  scale > 1
         else scale = hscale;
         if (scale > 1.0) {
            outww = outww * scale + 0.5;                                               //  round
            outhh = outhh * scale + 0.5;
            pxmout = PXM_rescale(pxmin,outww,outhh);                                   //  rescaled output file
            PXM_free(pxmin);
            pxmin = 0;
         }
      }

      if (*Arescale && pxmin)                                                          //  image rescale wanted                  25.1
      {
         scale = 1.0;
         if (strmatch(Arescale,"1/4")) scale = 0.25;                                   //  get rescale factor
         if (strmatch(Arescale,"1/3")) scale = 0.333333;
         if (strmatch(Arescale,"1/2")) scale = 0.5;
         if (strmatch(Arescale,"2/3")) scale = 0.666667;
         if (strmatch(Arescale,"1.0")) scale = 1.0;
         if (strmatch(Arescale,"1.4")) scale = 1.4;
         if (strmatch(Arescale,"2.0")) scale = 2.0;
         if (scale != 1.0) {
            outww = outww * scale + 0.5;                                               //  round
            outhh = outhh * scale + 0.5;
            pxmout = PXM_rescale(pxmin,outww,outhh);                                   //  rescaled output file
            PXM_free(pxmin);
            pxmin = 0;
         }
      }

      if (pxmin) {                                                                     //  no size change
         pxmout = pxmin;
         pxmin = 0;
      }

      err = PXM_save(pxmout,outfile[im],outbpc,jpeg_quality,0);                        //  write output file
      PXM_free(pxmout);
      if (err) {
         statmess[im] = TX("cannot create output file");
         continue;
      }

      if (Fcopymeta) {                                                                 //  copy metadata if requested
         err = meta_copy(infile[im],outfile[im],0,0,0);
         if (err) {
            statmess[im] = TX("metadata update error");
            continue;
         }
      }

      Fdelete2 = 0;                                                                    //  figure out if input file can be deleted
      if (Fdelete) Fdelete2 = 1;                                                       //  user says yes
      if (err) Fdelete2 = 0;                                                           //  not if error
      if (strmatch(infile[im],outfile[im])) Fdelete2 = 0;                              //  not if overwritten by output

      if (Fdelete2)                                                                    //  delete input file
         err = f_remove(infile[im],"delete");                                          //  file/index/thumb/gallery

      statmess[im] = TX("COMPLETED");
   }

   return 0;
}


//  dialog event and completion callback function

int batch_convert_dialog_event(zdialog *zd, ch *event)
{
   using namespace batch_convert_names;

   ch          *pp, *ploc, badplug[20];
   ch          countmess[80];
   int         ii, cc, yn, err;

   if (strmatch(event,"files"))                                                        //  select images to convert
   {
      zdialog_show(zd,0);                                                              //  hide parent dialog
      select_files(0);                                                                 //  get list of files to convert
      zdialog_show(zd,1);

      snprintf(countmess,80,TX("%d image files selected"),SFcount);                    //  update dialog
      zdialog_stuff(zd,"fcount",countmess);
   }

   if (strmatch(event,"browse")) {
      zdialog_fetch(zd,"newloc",newloc,XFCC);
      ploc = zgetfile(TX("Select folder"),MWIN,"folder",newloc);                       //  new location browse
      if (! ploc) return 1;
      zdialog_stuff(zd,"newloc",ploc);
      zfree(ploc);
   }

   if (zstrstr("tif png jxl jpg sametype",event)) {                                    //  jxl
      zdialog_stuff(zd,"tif",0);
      zdialog_stuff(zd,"png",0);
      zdialog_stuff(zd,"jxl",0);                                                       //  jxl
      zdialog_stuff(zd,"jpg",0);
      zdialog_stuff(zd,"sametype",0);
      zdialog_stuff(zd,event,1);
   }

   if (zstrstr("8-bit 16-bit samebpc",event)) {
      zdialog_stuff(zd,"8-bit",0);
      zdialog_stuff(zd,"16-bit",0);
      zdialog_stuff(zd,"samebpc",0);
      zdialog_stuff(zd,event,1);
   }

   zdialog_fetch(zd,"jpg",ii);                                                         //  if jpeg, force 8 bits/color
   if (ii) {
      zdialog_stuff(zd,"16-bit",0);
      zdialog_stuff(zd,"samebpc",0);
      zdialog_stuff(zd,"8-bit",1);
   }

   if (strstr("downsize upsize",event)) {                                              //  if upsize/downsize, no rescale        25.1
      zdialog_fetch(zd,"downsize",Fdownsize);
      zdialog_fetch(zd,"upsize",Fupsize);
      if (Fupsize || Fdownsize) zdialog_stuff(zd,"rs-1.0",1);
   }

   if (strmatchN(event,"rs-",3)) {                                                     //  if rescale, no upsize/downsize        25.1
      zdialog_fetch(zd,"rs-1.0",Frescale);
      if (Frescale != 1) {                                                             //  is rescale = 1.0 ?
         zdialog_stuff(zd,"upsize",0);                                                 //  no, no upsize/downsize
         zdialog_stuff(zd,"downsize",0);
      }
   }

   //  wait for dialog completion via [proceed] button

   if (zd->zstat != 1) return 1;
   zd->zstat = 0;                                                                      //  keep active until inputs OK

   zdialog_fetch(zd,"text1",text1,100);                                                //  text within file name
   zdialog_fetch(zd,"text2",text2,100);                                                //  replacement text
   zdialog_fetch(zd,"newname",newname,200);                                            //  new file name
   zdialog_fetch(zd,"baseseq",baseseq);                                                //  base sequence number
   zdialog_fetch(zd,"addseq",addseq);                                                  //  sequence number adder
   zdialog_fetch(zd,"newloc",newloc,XFCC);                                             //  new location (folder)
   zdialog_fetch(zd,"maxww",maxww);                                                    //  new max width
   zdialog_fetch(zd,"maxhh",maxhh);                                                    //  new max height
   zdialog_fetch(zd,"downsize",Fdownsize);                                             //  downsize checkbox
   zdialog_fetch(zd,"upsize",Fupsize);                                                 //  upsize checkbox

   zdialog_fetch(zd,"jpgqual",jpeg_quality);                                           //  jpeg quality
   zdialog_fetch(zd,"delete",Fdelete);                                                 //  delete originals
   zdialog_fetch(zd,"copymeta",Fcopymeta);                                             //  copy metadata
   zdialog_fetch(zd,"upalbums",Fupalbums);                                             //  update albums                         25.1

   zdialog_fetch(zd,"sametype",Fsametype);
   zdialog_fetch(zd,"jpg",ii);
   if (ii) strcpy(newext,".jpg");
   zdialog_fetch(zd,"tif",ii);
   if (ii) strcpy(newext,".tif");
   zdialog_fetch(zd,"png",ii);
   if (ii) strcpy(newext,".png");
   zdialog_fetch(zd,"jxl",ii);                                                         //  jxl
   if (ii) strcpy(newext,".jxl");

   zdialog_fetch(zd,"samebpc",Fsamebpc);
   zdialog_fetch(zd,"8-bit",ii);
   if (ii) newbpc = 8;
   zdialog_fetch(zd,"16-bit",ii);
   if (ii) newbpc = 16;

   if (! SFcount) {
      zmessageACK(Mwin,TX("no files selected"));
      return 1;
   }

   Fnewtext = 0;
   if (*text1) Fnewtext = 1;                                                           //  replace text1 with text2 (may be null)

   Fplugdate = Fplugseq = Fplugname = 0;

   strTrim2(newname);
   if (! blank_null(newname))
   {
      if (Fnewtext) {
         zmessageACK(Mwin,TX("you cannot use new name and replace text together"));
         return 1;
      }

      pp = newname;

      while ((pp = strchr(pp,'$')))
      {
         if (strmatchN(pp,"$yyyy",5)) Fplugdate = 1;                                   //  $yyyy
         else if (strmatchN(pp,"$mm",3)) Fplugdate = 1;                                //  $mm
         else if (strmatchN(pp,"$dd",3)) Fplugdate = 1;                                //  $dd
         else if (strmatchN(pp,"$s",2)) Fplugseq = 1;                                  //  $s...s   (sequence number cc)
         else if (strmatchN(pp,"$oldname",8)) Fplugname = 1;                           //  $oldname
         else {
            for (cc = 0; pp[cc] > ' ' && cc < 20; cc++);
            strncpy0(badplug,pp,cc);
            zmessageACK(Mwin,TX("invalid plugin: %s"),badplug);
            return 1;
         }
         pp++;
      }
   }

   if (! blank_null(newname) && ! Fplugname && ! Fplugseq) {
      zmessageACK(Mwin,TX("you must use either $s or $oldname"));
      return 1;
   }

   if (Fplugseq && (baseseq < 1 || addseq < 1)) {
      zmessageACK(Mwin,TX("$s plugin needs base and adder"));
      return 1;
   }

   if (! Fplugseq && (baseseq > 0 || addseq > 0)) {
      zmessageACK(Mwin,TX("base and adder need $s plugin"));
      return 1;
   }

   strTrim2(newloc);                                                                   //  check location
   if (! blank_null(newloc)) {
      cc = strlen(newloc) - 1;
      if (newloc[cc] == '/') newloc[cc] = 0;                                           //  remove trailing '/'
      err = check_create_dir(newloc);                                                  //  create if needed
      if (err) return 1;
   }

   if (Fdownsize || Fupsize) {                                                         //  downsize and/or upsize wanted,
      if (maxww < 20 || maxhh < 20                                                     //    check max. width and height values
          || maxww > wwhh_limit1 || maxhh > wwhh_limit1
          || maxww * maxhh > wwhh_limit2) {
         zmessageACK(Mwin,TX("max. width or height is not reasonable"));
         return 1;
      }
   }

   *Arescale = 0;
   zdialog_fetch(zd,"rs-1/4",Frescale);                                                //  get rescale option                    25.1
   if (Frescale) strcpy(Arescale,"1/4");
   zdialog_fetch(zd,"rs-1/3",Frescale);
   if (Frescale) strcpy(Arescale,"1/3");
   zdialog_fetch(zd,"rs-1/2",Frescale);
   if (Frescale) strcpy(Arescale,"1/2");
   zdialog_fetch(zd,"rs-2/3",Frescale);
   if (Frescale) strcpy(Arescale,"2/3");
   zdialog_fetch(zd,"rs-1.0",Frescale);
   if (Frescale) strcpy(Arescale,"1.0");
   zdialog_fetch(zd,"rs-1.4",Frescale);
   if (Frescale) strcpy(Arescale,"1.4");
   zdialog_fetch(zd,"rs-2.0",Frescale);
   if (Frescale) strcpy(Arescale,"2.0");
   if (strmatch(Arescale,"1.0")) *Arescale = 0;                                        //  rescale 1.0 --> no rescale            25.1

   if (*newname <= ' ' && *newloc <= ' ' && Fsametype) Freplace = 1;                   //  if no new name or location,
   else Freplace = 0;                                                                  //    assume input file is replaced

   if (Freplace && Fdelete) {
      zmessageACK(Mwin,TX("delete originals specified but no new name given"));
      return 1;
   }

   /**
         Convert NN image files                    CF
           Replace text: xxxx --> yyyy             RT
           Rename to xxxxxxx                       RN
           Convert to .ext  N-bits/color           CX  CXa
           Downsize within NNxNN                   DZ
           Upsize within NNxNN                     DZa
           Rescale to N.Nx                         RS                                  //  25.1
           Output to /.../...                      OP
           Copy Metadata                           CM
           Update Albums                           UA
           *** Delete Originals ***                DO
           *** Replace Originals ***               RO
         PROCEED?                                  PR
   **/

   ch    messCF[100], messRN[100], messRT[400], messCX[100], messCXa[100],             //  more room for big translations        26.0
         messDZ[100], messDZa[100], messRS[100],
         messOP[800], messCM[140], messUA[140],
         messDO[100], messRO[100], messPR[80];
   ch    warnmess[1000];

   *messCF = *messRN = *messRT = *messCX = *messCXa = *messDZ = *messDZa = *messRS
           = *messOP = *messCM = *messUA = *messDO = *messRO = *messPR = 0;

   snprintf(messCF,100,TX("Convert %d image files"),SFcount);
   if (Fnewtext) snprintf(messRT,400,TX("\n Replace text: %s --> %s"),text1,text2);
   if (*newname) snprintf(messRN,100,TX("\n Rename to %s"),newname);
   if (! Fsametype) snprintf(messCX,100,TX("\n Convert to %s"),newext);
   if (! Fsamebpc) snprintf(messCXa,100,TX("\n %d-bits/color"),newbpc);
   if (Fdownsize) snprintf(messDZ,100,TX("\n Downsize within %dx%d"),maxww,maxhh);
   if (Fupsize) snprintf(messDZa,100,TX("\n Upsize within %dx%d"),maxww,maxhh);
   if (*Arescale) snprintf(messRS,100,TX("\n Rescale to %sx"),Arescale);
   if (*newloc) snprintf(messOP,800,TX("\n Output to %s"),newloc);
   if (Fcopymeta) { strncpy0(messCM,TX("\n Copy Metadata"),140); }
   if (Fupalbums) { strncpy0(messUA,TX("\n Update Albums"),140); }
   if (Fdelete) strncpy0(messDO,TX("\n *** Delete Originals ***"),100);
   if (Freplace) strncpy0(messRO,TX("\n *** Replace Originals ***"),100);
   strncpy0(messPR,TX("\n\nPROCEED?"),80);

   snprintf(warnmess,1000,"%s %s %s %s %s %s %s %s %s %s %s %s %s %s",
            messCF, messRT, messRN, messCX, messCXa, messDZ, messDZa, messRS,
            messOP, messCM, messUA, messDO, messRO, messPR);

   yn = zmessageYN(Mwin,warnmess);
   if (! yn) return 1;

   zd->zstat = 1;                                                                      //  [proceed]
   return 1;
}


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

//  Move selected files to a new location (Fotocx indexed)

namespace batch_copy_move_names
{
   ch       newloc[XFCC];                                                              //  destination folder
   int      Fdelete;                                                                   //  delete originals
}


//  menu function

void m_batch_copy_move(GtkWidget *, ch *)
{
   using namespace batch_copy_move_names;

   int  batch_copy_move_dialog_event(zdialog *zd, ch *event);

   zdialog     *zd, *zdpop;
   int         ii, cc, err;
   int         zstat;
   ch          *pp, text[100];
   ch          *infile, *outfile;
   ch          **oldfiles, **newfiles;
   int         Noldnew;

   F1_help_topic = TX("BATCH COPY/MOVE");

   printf("m_batch_copy_move \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   if (Fblock("batch copy/move")) return;

/***
       ___________________________________________________________
      |                    Batch Copy/Move                        |
      |                                                           |
      |  [Select Files]  N files selected                         |
      |                                                           |
      |  New Location [_______________________________] [browse]  |
      |                                                           |
      |  [x] Delete Originals                                     |
      |                                                           |
      |                                             [Proceed] [X] |
      |___________________________________________________________|

***/

   zd = zdialog_new(TX("Batch Copy/Move"),Mwin,TX("Proceed"),"X",null);

   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","files","hbf",TX("Select Files"),"space=5");
   zdialog_add_widget(zd,"label","fcount","hbf",TX("no files selected"),"space=10");

   zdialog_add_widget(zd,"hbox","hbloc","dialog");
   zdialog_add_widget(zd,"label","labloc","hbloc",TX("New Location"),"space=5");
   zdialog_add_widget(zd,"zentry","newloc","hbloc",0,"expand");
   zdialog_add_widget(zd,"button","browse","hbloc",TX("Browse"),"space=5");

   zdialog_add_widget(zd,"hbox","hbdel","dialog");
   zdialog_add_widget(zd,"check","delete","hbdel",TX("Delete Originals"),"space=3");

   snprintf(text,100,TX("%d image files selected"),SFcount);                           //  show selected files count
   zdialog_stuff(zd,"fcount",text);

   zdialog_stuff(zd,"delete",0);                                                       //  delete originals - no

   *newloc = 0;                                                                        //  no new location

   oldfiles = newfiles = 0;                                                            //  list of old/new files
   Noldnew = 0;                                                                        //    for album updates

   zdialog_load_inputs(zd);                                                            //  preload prior user inputs
   zdialog_run(zd,batch_copy_move_dialog_event,"parent");                              //  run dialog
   zstat = zdialog_wait(zd);                                                           //  wait for completion
   zdialog_free(zd);
   if (zstat != 1) goto cleanup;                                                       //  canceled
   if (! SFcount) goto cleanup;                                                        //  no files selected

   free_resources();                                                                   //  no curr. file

   if (Fdelete) {
      cc = SFcount * sizeof(ch *);                                                     //  input files are to be deleted:
      oldfiles = (ch **) zmalloc(cc,"batch copy/move");                                //    reserve space to hold list of
      newfiles = (ch **) zmalloc(cc,"batch copy/move");                                //    old/new filespecs to update albums
   }

   cc = strlen(newloc);                                                                //  output location
   if (newloc[cc-1] == '/') newloc[cc-1] = 0;                                          //  remove trailing '/'

   zdpop = popup_report_open(TX("Processing files"),Mwin,600,300,0,0,0,"X",null);      //  log report

   zadd_locked(NFbusy,+1);

   for (ii = 0; ii < SFcount; ii++)                                                    //  loop selected files
   {
      zmainloop();                                                                     //  keep GTK alive

      if (! zdialog_valid(zdpop)) break;                                               //  report canceled

      infile = SelFiles[ii];                                                           //  input file

      popup_report_write2(zdpop,0,"\n");
      popup_report_write2(zdpop,0,"%s \n",infile);                                     //  log each input file

      if (image_file_type(infile) != IMAGE) {
         popup_report_write2(zdpop,0,TX("*** invalid file \n"));
         continue;
      }

      pp = strrchr(infile,'/');                                                        //  /filename.ext
      cc = strlen(newloc) + strlen(pp) + 2;
      outfile = (ch *) zmalloc(cc,"batch copy/move");
      strcpy(outfile,newloc);                                                          //  /newloc/.../filename.ext
      strcat(outfile,pp);

      popup_report_write2(zdpop,0,"%s \n",outfile);                                    //  log each output file

      if (regfile(outfile)) {                                                          //  check if file exists in new location
         popup_report_write2(zdpop,1,"%s \n",TX("output file exists"));
         zfree(outfile);
         continue;
      }

      err = cp_copy(infile,outfile);                                                   //  copy infile to outfile
      if (err) {
         popup_report_write2(zdpop,1,"%s \n",strerror(err));                           //  error, do nothing
         zfree(outfile);
         continue;
      }

      if (Fdelete)                                                                     //  delete input file
      {
         err = f_remove(infile,"delete");                                              //  file/index/thumb/gallery

         oldfiles[Noldnew] = zstrdup(infile,"batch copy/move");                        //  mark for updating albums
         newfiles[Noldnew] = zstrdup(outfile,"batch copy/move");
         Noldnew++;
      }

      zfree(outfile);
   }

   if (! zdialog_valid(zdpop))
      popup_report_write2(zdpop,0,TX("\n *** CANCELED \n"));
   else popup_report_write2(zdpop,0,TX("\n *** COMPLETED \n"));
   popup_report_bottom(zdpop);

   zadd_locked(NFbusy,-1);

   index_rebuild(2,0);                                                                 //  update index and thumbnails           25.1

   if (Noldnew) {                                                                      //  update albums for renamed/moved files
      popup_report_write2(zdpop,0,TX("updating albums ...\n"));
      album_purge_replace("ALL",Noldnew,oldfiles,newfiles);
   }

cleanup:

   Fblock(0);

   if (Noldnew) {                                                                      //  free memory
      for (ii = 0; ii < Noldnew; ii++) {
         zfree(oldfiles[ii]);
         zfree(newfiles[ii]);
      }
      if (oldfiles) zfree(oldfiles);
      if (newfiles) zfree(newfiles);
      Noldnew = 0;
   }

   if (*newloc) gallery(newloc,"init",0);                                              //  refresh file list                     25.1
   else gallery(0,"init",0);
   gallery(0,"sort",-2);                                                               //  recall sort and position
   gallery(0,"paint",-1);                                                              //  repaint from same position
   viewmode('G');

   return;
}


//  dialog event and completion callback function

int batch_copy_move_dialog_event(zdialog *zd, ch *event)
{
   using namespace batch_copy_move_names;

   ch          countmess[80];
   ch          *ploc;
   int         cc, yn, err;

   if (strmatch(event,"files"))                                                        //  select images to convert
   {
      zdialog_show(zd,0);                                                              //  hide parent dialog
      select_files(0);                                                                 //  get list of files to copy
      zdialog_show(zd,1);

      snprintf(countmess,80,TX("%d image files selected"),SFcount);                    //  update dialog
      zdialog_stuff(zd,"fcount",countmess);
   }

   if (strmatch(event,"browse")) {
      zdialog_fetch(zd,"newloc",newloc,XFCC);
      ploc = zgetfile(TX("Select folder"),MWIN,"folder",newloc);                       //  new location browse
      if (! ploc) return 1;
      zdialog_stuff(zd,"newloc",ploc);
      zfree(ploc);
   }

   if (zd->zstat != 1) return 1;                                                       //  not [proceed]

//  proceed with copy or move

   zd->zstat = 0;                                                                      //  keep active until inputs OK

   if (! SFcount) {
      zmessageACK(Mwin,TX("no files selected"));
      return 1;
   }

   zdialog_fetch(zd,"newloc",newloc,XFCC);
   zdialog_fetch(zd,"delete",Fdelete);                                                 //  delete originals

   strTrim2(newloc);                                                                   //  check location
   cc = strlen(newloc) - 1;
   if (newloc[cc] == '/') newloc[cc] = 0;                                              //  remove trailing '/'
   err = check_create_dir(newloc);                                                     //  create if needed
   if (err) return 1;

   ch    mess0[100], mess1[800], mess2[100], mess3[100];                               //  enlarge for translations              26.0
   ch    warnmess[1000];

   *mess0 = *mess1 = *mess2 = *mess3 = 0;
   
   if (Fdelete) {                                                                      //  clarified text                        26.0
      snprintf(mess0,100,TX("Move %d image files"),SFcount);
      snprintf(mess1,800,TX("\n New Location: %s"),newloc);
      strncpy0(mess2,TX("\n *** Delete Originals ***"),100);
      strncpy0(mess3,TX("\n PROCEED?"),100);
   }
   
   else {
      snprintf(mess0,100,TX("Copy %d image files"),SFcount);
      snprintf(mess1,800,TX("\n New Location: %s"),newloc);
      strncpy0(mess2,TX("\n *** Keep Originals ***"),100);
      strncpy0(mess3,TX("\n PROCEED?"),100);
   }

   snprintf(warnmess,1000,"%s %s %s %s",mess0,mess1,mess2,mess3);

   yn = zmessageYN(Mwin,warnmess);
   if (! yn) return 1;

   zd->zstat = 1;                                                                      //  [proceed]
   return 1;
}


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

//  Batch upright image files.
//  Look for files rotated 90˚ (according to metadata) and upright them.

int      Fbupall;

void m_batch_upright(GtkWidget *, ch *)
{
   int  batch_upright_dialog_event(zdialog *zd, ch *event);

   zdialog        *zd, *zdpop;
   int            zstat;
   ch             *infile, *newfile, *delfile;
   ch             *pp, *ppv[1], text[100];
   int            ii, err, yn;
   int            Fcount, fwarn;
   int            angle, mirror;
   ch             *metatag[1] = { meta_orientation_tag };
   ch             orient;

   F1_help_topic = TX("BATCH UPRIGHT");

   printf("m_batch_upright \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   if (Fblock("batch upright")) return;

/***
       ___________________________________
      |       Batch Upright               |
      |                                   |
      |  [Select Files]  N files selected |
      |  [x] Survey all files             |
      |                                   |
      |                     [Proceed] [X] |
      |___________________________________|

***/

   Fbupall = 0;

   zd = zdialog_new("Batch Upright",Mwin,TX("Proceed"),"X",null);

   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","files","hbf",TX("Select Files"),"space=5");
   zdialog_add_widget(zd,"label","fcount","hbf",TX("no files selected"),"space=10");
   zdialog_add_widget(zd,"hbox","hbaf","dialog");
   zdialog_add_widget(zd,"check","allfiles","hbaf",TX("Survey all files"),"space=5");

   snprintf(text,100,TX("%d image files selected"),SFcount);                           //  show selected files count
   zdialog_stuff(zd,"fcount",text);

   zdialog_run(zd,batch_upright_dialog_event,"parent");                                //  run dialog
   zstat = zdialog_wait(zd);                                                           //  wait for completion
   zdialog_free(zd);
   if (zstat != 1) goto cleanup;                                                       //  canceled
   if (! Fbupall && ! SFcount) goto cleanup;                                           //  nothing selected

   free_resources();                                                                   //  no curr. file

   fwarn = 0;

   if (Fbupall)                                                                        //  "survey all files" selected
   {
      for (ii = 0; ii < Nxxrec; ii++) {
         pp = strrchr(xxrec_tab[ii]->file,'.');
         if (pp && strcasestr(".jp2 .heic .avif .webp",pp)) break;
      }
      if (ii < Nxxrec) fwarn = 1;
   }

   else if (SFcount)
   {
      for (ii = 0; ii < SFcount; ii++) {
         pp = strrchr(SelFiles[ii],'.');
         if (pp && strcasestr(".jp2 .heic .avif .webp",pp)) break;
      }
      if (ii < SFcount) fwarn = 1;
   }

   if (fwarn) {
      yn = zmessageYN(Mwin,TX("file types: .jp2 .heic .avif .webp \n"
                              "will become: .jpg - CONTINUE?"));
      if (! yn) goto cleanup;
   }

   zdpop = popup_report_open(TX("Processing files"),Mwin,500,200,0,0,0,"X",null);      //  log report

   zadd_locked(NFbusy,+1);

   if (Fbupall) Fcount = Nxxrec;
   else Fcount = SFcount;

   for (ii = 0; ii < Fcount; ii++)
   {
      zmainloop();                                                                     //  keep GTK alive

      if (! zdialog_valid(zdpop)) break;                                               //  report canceled

      if (Fbupall) infile = xxrec_tab[ii]->file;
      else infile = SelFiles[ii];

      popup_report_write2(zdpop,0,"%s \n",infile);                                     //  log each input file

      if (image_file_type(infile) != IMAGE) {
         popup_report_write2(zdpop,0,TX("*** invalid file \n"));
         continue;
      }

      meta_get(infile,(ch **) metatag,ppv,1);                                          //  get metadata Orientation
      if (! ppv[0]) continue;
      orient = *ppv[0];
      zfree(ppv[0]);
      if (orient < '2' || orient > '8') continue;                                      //  not rotated

      angle = mirror = 0;

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

      err = f_open(infile);
      if (err) {
         popup_report_write2(zdpop,0,TX("*** cannot open input file"));
         continue;
      }

      E0pxm = PXM_load(curr_file,0);                                                   //  load poss. 16-bit image
      if (! E0pxm) {
         popup_report_write2(zdpop,0,TX("*** cannot read input file"));
         continue;
      }

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

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

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

   zadd_locked(NFbusy,-1);

   if (! zdialog_valid(zdpop)) printf("*** report canceled \n");
   else popup_report_write2(zdpop,0,TX("\n *** COMPLETED \n"));
   popup_report_bottom(zdpop);

cleanup:

   Fblock(0);

   gallery(0,"init",0);                                                                //  refresh gallery                       25.1
   gallery(0,"sort",-2);                                                               //  recall sort and position
   gallery(0,"paint",-1);                                                              //  repaint from same position
   viewmode('G');

   return;
}


//  dialog event and completion callback function

int batch_upright_dialog_event(zdialog *zd, ch *event)
{
   ch        countmess[80];

   if (strmatch(event,"files"))                                                        //  select images to convert
   {
      zdialog_show(zd,0);                                                              //  hide parent dialog
      select_files(0);                                                                 //  get new list
      zdialog_show(zd,1);

      snprintf(countmess,80,TX("%d image files selected"),SFcount);                    //  update dialog
      zdialog_stuff(zd,"fcount",countmess);
      zdialog_stuff(zd,"allfiles",0);

      if (! SFcount) return 1;
   }

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

   zdialog_fetch(zd,"allfiles",Fbupall);                                               //  get "survey all" option

   if (! Fbupall && ! SFcount) {                                                       //  nothing selected
      zmessageACK(Mwin,TX("no files selected"));
      zd->zstat = 0;                                                                   //  keep dialog active
   }

   if (Fbupall && SFcount) {
      zmessageACK(Mwin,TX("both 'All Files' and specific files were selected"));
      zd->zstat = 0;
   }

   return 1;
}


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

//  Batch delete or trash image files.

int      bdt_option;                                                                   //  1/2 = delete/trash

void m_batch_deltrash(GtkWidget *, ch *)
{
   int  batch_deltrash_dialog_event(zdialog *zd, ch *event);

   zdialog     *zd, *zdpop;
   int         zstat, nn, err;
   ch          *file, text[100];
   int         ii;

   F1_help_topic = TX("BATCH DELETE/TRASH");

   printf("m_batch_deltrash \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   if (Fblock("batch delete/trash")) return;

/***
       ___________________________________
      |       Batch Delete/Trash          |
      |                                   |
      |  [Select Files]  N files selected |
      |  (o) delete    (o) trash          |
      |                                   |
      |                     [Proceed] [X] |
      |___________________________________|

***/

   zd = zdialog_new(TX("Batch Delete/Trash"),Mwin,TX("Proceed"),"X",null);
   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","files","hbf",TX("Select Files"),"space=5");
   zdialog_add_widget(zd,"label","fcount","hbf",TX("no files selected"),"space=10");
   zdialog_add_widget(zd,"hbox","hbdt","dialog");
   zdialog_add_widget(zd,"label","labdel","hbdt",TX("delete"),"space=5");
   zdialog_add_widget(zd,"radio","delete","hbdt",0);
   zdialog_add_widget(zd,"label","space","hbdt",0,"space=10");
   zdialog_add_widget(zd,"label","labtrash","hbdt",TX("trash"),"space=5");
   zdialog_add_widget(zd,"radio","trash","hbdt",0);

   bdt_option = 2;

   snprintf(text,100,TX("%d image files selected"),SFcount);                           //  show selected files count
   zdialog_stuff(zd,"fcount",text);

   zdialog_stuff(zd,"delete",0);
   zdialog_stuff(zd,"trash",1);

   zdialog_run(zd,batch_deltrash_dialog_event,"parent");                               //  run dialog
   zstat = zdialog_wait(zd);                                                           //  wait for completion

   zdialog_fetch(zd,"delete",bdt_option);                                              //  get delete/trash option
   if (! bdt_option) bdt_option = 2;

   zdialog_free(zd);
   if (zstat != 1) goto finish;                                                        //  canceled
   if (! SFcount) goto finish;

   free_resources();                                                                   //  no curr. file

   zdpop = popup_report_open(TX("Processing files"),Mwin,500,200,0,0,0,"X",null);      //  log report

   zadd_locked(NFbusy,+1);

   for (ii = 0; ii < SFcount; ii++)                                                    //  loop selected files
   {
      zmainloop();                                                                     //  keep GTK alive

      if (! zdialog_valid(zdpop)) break;                                               //  report canceled

      file = SelFiles[ii];                                                             //  log each file
      popup_report_write2(zdpop,0,"%s \n",file);

      if (! regfile(file)) {                                                           //  file exists?
         popup_report_write2(zdpop,1,TX("file not found \n"));
         continue;
      }

      err = 0;
      if (bdt_option == 1)                                                             //  delete file
         err = f_remove(file,"delete");                                                //  file/index/thumb/gallery
      if (bdt_option == 2)                                                             //  move file to trash
         err = f_remove(file,"trash");
      if (err) {
         popup_report_write2(zdpop,1,TX("move to trash failed \n"));                   //  gnome trash failed
         nn = zdialog_choose(Mwin,"parent",TX("continue?"),TX("Yes"),TX("Quit"),null);
         if (nn == 2) {                                                                //  quit
            zdpop->zstat = 1;
            break;
         }
      }
      album_purge_replace("ALL",file,0);                                               //  remove from albums                    25.1
   }

   zadd_locked(NFbusy,-1);

   if (! zdialog_valid(zdpop)) printf("*** report canceled \n");
   else popup_report_write2(zdpop,0,TX("\n *** COMPLETED \n"));
   popup_report_bottom(zdpop);

   index_rebuild(2,0);                                                                 //  update index and thumbnails           25.1

finish:

   Fblock(0);

   gallery(0,"init",0);                                                                //  refresh gallery                       25.1
   gallery(0,"sort",-2);                                                               //  recall sort and position
   gallery(0,"paint",-1);                                                              //  repaint from same position
   viewmode('G');

   return;
}


//  dialog event and completion callback function

int batch_deltrash_dialog_event(zdialog *zd, ch *event)
{
   ch           countmess[80];
   int          ii;

   if (strmatch(event,"files"))                                                        //  select images to convert
   {
      zdialog_show(zd,0);                                                              //  hide parent dialog
      select_files(0);                                                                 //  get files
      zdialog_show(zd,1);

      snprintf(countmess,80,TX("%d image files selected"),SFcount);                    //  update dialog
      zdialog_stuff(zd,"fcount",countmess);
   }

   if (strmatch(event,"delete")) {                                                     //  delete radio button
      zdialog_fetch(zd,"delete",ii);
      if (ii) bdt_option = 1;
      zdialog_stuff(zd,"trash",0);
   }

   if (strmatch(event,"trash")) {                                                      //  trash radio button
      zdialog_fetch(zd,"trash",ii);
      if (ii) bdt_option = 2;
      zdialog_stuff(zd,"delete",0);
   }

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

   if (! SFcount) {                                                                    //  nothing selected
      zmessageACK(Mwin,TX("no files selected"));
      zd->zstat = 0;                                                                   //  keep dialog active
   }

   return 1;
}


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

//  Add a small image file over selected host files (for author, copyright, etc.).
//  Added image position is designated % from host image top and left.
//  Added image width is designated % of host image width.
//  Added image can have transparency.

namespace batch_overlay
{
   ch       *ovfile = 0;                                 //  overlay image file
   PXB      *ovfilepxb = 0;                              //  overlay file PXB
   int      pcttop, pctleft;                             //  overlay position, % from top, % from left, 1-99
   int      pctwidth;                                    //  overlay width, % host image width, 5-95
   int      Fwinadj;                                     //  flag, adjust overlay width for window size
   int      winww, winhh;                                //  window size (current or user input)
   int      Frepl, Fvers;                                //  flags, replace host files or make new versions
};


//  menu function

void m_batch_overlay(GtkWidget *, ch *)
{
   using namespace batch_overlay;

   int  batch_overlay_dialog_event(zdialog *zd, ch *event);

   zdialog     *zd, *zdpop;
   int         zstat;
   ch          *infile = 0, *outfile = 0;
   ch          text[100], *pp;
   int         ii, err;
   PXM         *infilepxm = 0;
   PXB         *ovpxb = 0;
   int         ovww, ovhh;
   int         ovorgx, ovorgy;
   int         px1, py1, px2, py2;
   uint8       *pix1;
   float       *pix2;
   float       f1, f2;
   float       winAR, infileAR;                                                        //  window and host image ww/hh ratios

   F1_help_topic = TX("BATCH OVERLAY");

   printf("m_batch_overlay \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   if (Fblock("batch overlay")) return;

/***
          __________________________________________________
         |               Batch Overlay Image                |
         |                                                  |
         |  [Select host image files]  no files selected    |           button: hostselect
         |  [Select overlay file]  no file selected         |           button: ovselect
         |  - - - - - - - - - - - - - - - - - - - - - - - - |
         |  Overlay position in host image:                 |
         |      % from top: [__]   % from left: [__]        |           zspin: pcttop  pctleft
         |  - - - - - - - - - - - - - - - - - - - - - - - - |
         |  Overlay width, % host image width: [__]         |           zspin: pctwidth
         |  [x] Make width constant for window size:        |           check: Fwinadj
         |      width [____]  height [____]   [_] refresh]  |           zspin: winww, winhh  zbutton: refresh
         |  - - - - - - - - - - - - - - - - - - - - - - - - |
         |  [x] Replace host files  [x] Make new versions   |           check: Frepl  check: Fvers
         |                                                  |
         |                                    [Proceed] [X] |
         |__________________________________________________|

***/

   zd = zdialog_new("Batch Overlay",Mwin,TX("Proceed"),"X",null);

   zdialog_add_widget(zd,"hbox","hbhf","dialog",0,"space=2");
   zdialog_add_widget(zd,"button","hostselect","hbhf",TX("Select host image files"),"space=5");
   zdialog_add_widget(zd,"label","hostcount","hbhf",TX("no files selected"),"space=10");

   zdialog_add_widget(zd,"hbox","hbovf","dialog",0,"space=2");
   zdialog_add_widget(zd,"button","ovselect","hbovf",TX("Select overlay file"),"space=5");
   zdialog_add_widget(zd,"label","ovfile","hbovf",TX("no file selected"),"space=10");

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

   zdialog_add_widget(zd,"hbox","hbpos1","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labpos1","hbpos1",TX("Overlay position in host image:"),"space=5");
   zdialog_add_widget(zd,"hbox","hbpos2","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","space","hbpos2","","space=10");
   zdialog_add_widget(zd,"label","labtop","hbpos2",TX("% from top:"),"space=2");
   zdialog_add_widget(zd,"zspin","pcttop","hbpos2","1|99|1|99","space=2");
   zdialog_add_widget(zd,"label","space","hbpos2","","space=10");
   zdialog_add_widget(zd,"label","lableft","hbpos2",TX("% from left:"),"space=2");
   zdialog_add_widget(zd,"zspin","pctleft","hbpos2","1|99|1|99","space=2");

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

   zdialog_add_widget(zd,"hbox","hbwidth1","dialog");
   zdialog_add_widget(zd,"label","labwidth1","hbwidth1",TX("Overlay width, % host image width:"),"space=5");
   zdialog_add_widget(zd,"zspin","pctwidth","hbwidth1","5|95|1|20","space=2");
   zdialog_add_widget(zd,"hbox","hbwidth2","dialog");
   zdialog_add_widget(zd,"check","Fwinadj","hbwidth2",TX("Make width constant for window size"),"space=6");
   zdialog_add_widget(zd,"hbox","hbwidth3","dialog");
   zdialog_add_widget(zd,"label","space","hbwidth3","","space=10");
   zdialog_add_widget(zd,"label","labwinww","hbwidth3",TX("width"),"space=2");
   zdialog_add_widget(zd,"zspin","winww","hbwidth3","100|9999|1|2000","space=2");
   zdialog_add_widget(zd,"label","space","hbwidth3","","space=10");
   zdialog_add_widget(zd,"label","labwinhh","hbwidth3",TX("height"),"space=2");
   zdialog_add_widget(zd,"zspin","winhh","hbwidth3","100|9999|1|1000","space=2");
   zdialog_add_widget(zd,"zbutton","refresh","hbwidth3",TX("refresh"),"space=15");
   zdialog_add_ttip(zd,"refresh",TX("set current window size"));

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

   zdialog_add_widget(zd,"hbox","hbrepl","dialog",0,"space=1");
   zdialog_add_widget(zd,"check","Frepl","hbrepl",TX("Replace host files"),"space=6");
   zdialog_add_widget(zd,"check","Fvers","hbrepl",TX("Make new versions"),"space=6");

   if (SFcount) {
      snprintf(text,100,TX("%d image files selected"),SFcount);                        //  show selected files count
      zdialog_stuff(zd,"hostcount",text);
   }

   if (ovfile) {                                                                       //  show overlay file if known
      pp = strrchr(ovfile,'/');
      zdialog_stuff(zd,"ovfile",pp+1);
   }

   pctwidth = 20;                                                                      //  match dialog defaults
   pcttop = 99;
   pctleft = 99;

   Fwinadj = 0;
   gtk_window_get_size(MWIN,&winww,&winhh);
   zdialog_stuff(zd,"winww",winww);                                                    //  default window = current size
   zdialog_stuff(zd,"winhh",winhh);

   Frepl = 0;
   Fvers = 1;                                                                          //  default new version
   zdialog_stuff(zd,"Frepl",Frepl);
   zdialog_stuff(zd,"Fvers",Fvers);

   if (ovfile && ! ovfilepxb)
      ovfilepxb = PXB_load(ovfile,1);

   zdialog_run(zd,batch_overlay_dialog_event,"parent");                                //  run dialog
   zstat = zdialog_wait(zd);                                                           //  wait for completion
   zdialog_free(zd);

   if (zstat != 1) {                                                                   //  canceled
      printf("*** canceled \n");
      Fblock(0);
      return;
   }

   free_resources();                                                                   //  no current file

   zdpop = popup_report_open(TX("Processing files"),Mwin,600,300,0,0,0,"X",null);      //  log report

   zadd_locked(NFbusy,+1);

   for (ii = 0; ii < SFcount; ii++)                                                    //  loop selected files
   {
      zmainloop();                                                                     //  keep GTK alive

      if (outfile) zfree(outfile);
      outfile = 0;

      if (infilepxm) PXM_free(infilepxm);
      infilepxm = 0;

      if (! zdialog_valid(zdpop)) break;                                               //  report canceled

      infile = SelFiles[ii];                                                           //  input file

      if (image_file_type(infile) != IMAGE) {
         popup_report_write2(zdpop,0,TX("*** invalid file \n"));
         continue;
      }

      popup_report_write2(zdpop,0,"\n");
      popup_report_write2(zdpop,0,"%s \n",infile);                                     //  log each input file

      outfile = f_realpath(infile);                                                    //  outfile = infile
      if (! outfile) {
         popup_report_write2(zdpop,0,TX("*** cannot get real path of input file \n"));
         continue;
      }

      if (Fvers) {
         pp = file_new_version(outfile);                                               //  outfile is new version
         zfree(outfile);
         outfile = pp;
         if (! outfile) {
            popup_report_write2(zdpop,0,TX("*** cannot make new version of input file \n"));
            continue;
         }
      }

      infilepxm = PXM_load(infile,0);                                                  //  load input host file
      if (! infilepxm) {
         popup_report_write2(zdpop,0,TX("*** cannot load input file \n"));
         continue;
      }

      ovww = 0.01 * pctwidth * infilepxm->ww + 0.5;                                    //  overlay width, % host image width

      if (Fwinadj) {                                                                   //  increase overlay width if left/right
         infileAR = 1.0 * infilepxm->ww / infilepxm->hh;                               //    margins needed for 'tall' image
         winAR = 1.0 * winww / winhh;
         if (winAR > infileAR) ovww = ovww * winAR / infileAR;
      }

      if (ovww < 20) ovww = 20;                                                        //  sanity limit

      ovhh = 1.0 * ovfilepxb->hh * ovww / ovfilepxb->ww + 0.5;                         //  overlay image height

      if (ovpxb) PXB_free(ovpxb);
      ovpxb = PXB_rescale(ovfilepxb,ovww,ovhh);                                        //  rescale overlay image

      ovorgx = 0.01 * pctleft * (infilepxm->ww - ovww);                                //  overlay posn from host image left edge
      ovorgy = 0.01 * pcttop * (infilepxm->hh - ovhh);                                 //  overlay posn from host image top edge

      for (py1 = 0; py1 < ovhh; py1++)                                                 //  loop overlay image pixels
      for (px1 = 0; px1 < ovww; px1++)
      {
         px2 = ovorgx + px1;                                                           //  corresp. host image pixel
         py2 = ovorgy + py1;

         pix1 = PXBpix(ovpxb,px1,py1);                                                 //  overlay image pixel
         pix2 = PXMpix(infilepxm,px2,py2);                                             //  host image pixel

         if (ovpxb->nc == 4) f1 = pix1[3] / 256.0;                                     //  use transparency if present
         else f1 = 1.0;
         f2 = 1.0 - f1;

         pix2[0] = f1 * pix1[0] + f2 * pix2[0];                                        //  copy pixel
         pix2[1] = f1 * pix1[1] + f2 * pix2[1];
         pix2[2] = f1 * pix1[2] + f2 * pix2[2];
      }

      err = PXM_save(infilepxm,outfile,f_load_bpc,90,0);
      if (err) {
         popup_report_write2(zdpop,0,TX("*** cannot save output file \n"));
         continue;
      }

      popup_report_write2(zdpop,0,TX("*** completed \n"));
   }

   zadd_locked(NFbusy,-1);

   if (! zdialog_valid(zdpop))
      printf("*** report canceled \n");
   else popup_report_write2(zdpop,0,TX("\n *** COMPLETED \n"));
   popup_report_bottom(zdpop);

   index_rebuild(2,0);                                                                 //  update index and thumbnails           25.1

   Fblock(0);

   if (outfile) zfree(outfile);
   outfile = 0;

   if (infilepxm) PXM_free(infilepxm);
   infilepxm = 0;

   if (ovfilepxb) PXB_free(ovfilepxb);
   ovfilepxb = 0;

   if (ovpxb) PXB_free(ovpxb);
   ovpxb = 0;

   gallery(0,"init",0);                                                                //  refresh file list                     25.1
   gallery(0,"sort",-2);                                                               //  recall sort and position
   gallery(0,"paint",-1);                                                              //  repaint from same position
   viewmode('G');

   return;
}


//  dialog event and completion callback function

int batch_overlay_dialog_event(zdialog *zd, ch *event)
{
   using namespace batch_overlay;

   ch       countmess[80];
   ch       *pp, *ofile;

   if (zd->zstat)                                                                      //  dialog completed
   {
      if (zd->zstat != 1) return 1;                                                    //  canceled

      if (! SFcount) {                                                                 //  selected host files count
         zmessageACK(Mwin,TX("no host files selected"));
         zd->zstat = 0;
         return 1;
      }

      if (! ovfilepxb) {                                                               //  selected overlay file
         zmessageACK(Mwin,TX("no overlay file selected"));
         zd->zstat = 0;
         return 1;
      }

      return 1;                                                                        //  finished
   }

   if (strmatch(event,"hostselect")) {
      zdialog_show(zd,0);                                                              //  hide parent dialog
      select_files(0);                                                                 //  get list of files to convert
      zdialog_show(zd,1);
      snprintf(countmess,80,TX("%d image files selected"),SFcount);                    //  update dialog
      zdialog_stuff(zd,"hostcount",countmess);
   }

   if (strmatch(event,"ovselect"))                                                     //  select overlay image file
   {
      zdialog_show(zd,0);                                                              //  hide main dialog
      if (ovfile) ofile = select_files1(ovfile);
      else ofile = select_files1(saved_areas_folder);
      zdialog_show(zd,1);
      gallery(0,"paint",-1);                                                           //  repaint from same position

      if (! ofile) return 1;

      if (image_file_type(ofile) != IMAGE) {
         zmessageACK(Mwin,TX("not an image file"));
         return 1;
      }

      if (ovfilepxb) PXB_free(ovfilepxb);                                              //  create overlay PXB pixmap
      ovfilepxb = PXB_load(ofile,1);
      if (! ovfilepxb) return 1;

      if (ovfile) zfree(ovfile);                                                       //  selected file --> dialog
      ovfile = ofile;
      pp = strrchr(ovfile,'/');
      zdialog_stuff(zd,"ovfile",pp+1);
   }

   if (strmatch(event,"pctwidth"))                                                     //  overlay width % value
      zdialog_fetch(zd,"pctwidth",pctwidth);

   if (strmatch(event,"pcttop"))                                                       //  overlay position from top
      zdialog_fetch(zd,"pcttop",pcttop);

   if (strmatch(event,"pctleft"))                                                      //  overlay position from left
      zdialog_fetch(zd,"pctleft",pctleft);

   if (strmatch(event,"Fwinadj"))                                                      //  adjust overlay width for window
      zdialog_fetch(zd,"Fwinadj",Fwinadj);

   if (strmatch(event,"winww"))                                                        //  window width
      zdialog_fetch(zd,"winww",winww);

   if (strmatch(event,"winhh"))                                                        //  window height
      zdialog_fetch(zd,"winhh",winhh);

   if (strmatch(event,"refresh")) {                                                    //  refresh current window size
      gtk_window_get_size(MWIN,&winww,&winhh);
      zdialog_stuff(zd,"winww",winww);                                                 //  target window = current size
      zdialog_stuff(zd,"winhh",winhh);
   }

   if (strmatch(event,"Frepl")) {                                                      //  replace host files
      zdialog_fetch(zd,"Frepl",Frepl);
      Fvers = 1 - Frepl;
      zdialog_stuff(zd,"Fvers",Fvers);
   }

   if (strmatch(event,"Fvers")) {                                                      //  make new versions
      zdialog_fetch(zd,"Fvers",Fvers);
      Frepl = 1 - Fvers;
      zdialog_stuff(zd,"Frepl",Frepl);
   }

   return 1;
}


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

//  Select files and output a file containing the selected file names.

namespace imagelist_images
{
   ch       outfile[300];
};


//  menu function

void m_export_filelist(GtkWidget *, ch *)
{
   using namespace imagelist_images;

   int   export_filelist_dialog_event(zdialog *zd, ch *event);

   zdialog  *zd;
   FILE     *fid;
   int      ii, zstat;
   ch       *title = TX("Create a file of selected image files");
   ch       text[100];

   F1_help_topic = TX("EXPORT FILE LIST");

   printf("m_export_filelist \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   if (Fblock("export file list")) return;

/***
       ____________________________________________
      |    Create a file of selected image files   |
      |                                            |
      |  [Select Files]  NNN files selected        |
      |  Output File [__________________] [Browse] |
      |                                            |
      |                              [Proceed] [X] |
      |____________________________________________|

***/

   zd = zdialog_new(title,Mwin,TX("Proceed"),"X",null);

   zdialog_add_widget(zd,"hbox","hbif","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","infiles","hbif",TX("Select Files"),"space=3");
   zdialog_add_widget(zd,"label","Nfiles","hbif",TX("no files selected"),"space=10");

   zdialog_add_widget(zd,"hbox","hbof","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labof","hbof",TX("Output File"),"space=3");
   zdialog_add_widget(zd,"zentry","outfile","hbof",0,"size=30|space=5");
   zdialog_add_widget(zd,"button","browse","hbof",TX("Browse"),"space=5");

   snprintf(text,100,TX("%d image files selected"),SFcount);                           //  show selected files count
   zdialog_stuff(zd,"Nfiles",text);

   if (*outfile) zdialog_stuff(zd,"outfile",outfile);

   zdialog_run(zd,export_filelist_dialog_event,"parent");                              //  run dialog, wait for response

retry:
   zstat = zdialog_wait(zd);
   if (zstat != 1) {
      zdialog_free(zd);
      Fblock(0);
      return;
   }

   free_resources();                                                                   //  no current file

   zdialog_fetch(zd,"outfile",outfile,300);                                            //  get output file from dialog

   if (! SFcount) {
      zmessageACK(Mwin,TX("no input files selected"));
      zd->zstat = 0;
      goto retry;                                                                      //  no input files
   }

   if (! *outfile) {
      zmessageACK(Mwin,TX("no output file selected"));
      zd->zstat = 0;
      goto retry;                                                                      //  no input files
   }

   fid = fopen(outfile,"w");                                                           //  open output file
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));                                               //  error
      zd->zstat = 0;
      goto retry;                                                                      //  no input files
   }

   zdialog_free(zd);

   for (ii = 0; ii < SFcount; ii++)                                                    //  write input file names to output
      fprintf(fid,"%s\n",SelFiles[ii]);

   fclose(fid);

   zmessageACK(Mwin,TX("COMPLETED"));

   Fblock(0);
   return;
}


//  dialog event and completion function

int export_filelist_dialog_event(zdialog *zd, ch *event)
{
   using namespace imagelist_images;

   ch       *file;
   ch       countmess[80];

   if (strmatch(event,"infiles"))                                                      //  select image files
   {
      select_files(0);                                                                 //  get list of files to export
      snprintf(countmess,80,TX("%d image files selected"),SFcount);                    //  update dialog
      zdialog_stuff(zd,"Nfiles",countmess);
   }

   if (strmatch(event,"browse"))
   {
      file = zgetfile(TX("Output File"),MWIN,"save",outfile,0);
      if (file) zdialog_stuff(zd,"outfile",file);
      else zdialog_stuff(zd,"outfile","");
   }

   return 1;
}


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

//  Export selected image files to another folder with optional downsizing.
//  Only the metadata relevant for web photo services is copied.

namespace export_files_names
{
   ch       tolocation[XFCC];
   int      Fsamesize;
   int      maxww, maxhh;
   int      Fmeta;
};


//  menu function

void m_export_files(GtkWidget*, ch *menu)
{
   using namespace export_files_names;

   int export_files_dialog_event(zdialog *zd, ch *event);

   zdialog     *zd, *zdpop;
   int         ii, cc, err, ww, hh, zstat;
   PXM         *pxmin, *pxmout;
   ch          *infile, *outfile, *pp, text[100];
   float       scale, wscale, hscale;
   #define     NK 5
   ch          *tagnames[NK] = { meta_pdate_tag, meta_keywords_tag,
               meta_copyright_tag, meta_desc_tag, meta_title_tag };
   ch          *tagvals[NK];

   F1_help_topic = TX("EXPORT FILES");

   printf("m_export_files \n");

   if (Xindexlev < 1) {
      index_rebuild(1,0);                                                              //  25.1
      if (Nxxrec == 0) {
         zmessageACK(Mwin,TX("image index required"));
         return;
      }
   }

   if (Fblock("export files")) return;

/***
       __________________________________________________
      |                Export Files                      |
      |                                                  |
      |  [Select Files]  N files selected                |
      |  To Location [________________________] [Browse] |
      |  Max. Width [____]  Height [____]  [x] no change |
      |  [x] export metadata                             |
      |                                                  |
      |                                    [Proceed] [X] |
      |__________________________________________________|

***/

   zd = zdialog_new("Export Files",Mwin,TX("Proceed"),"X",null);
   zdialog_add_widget(zd,"hbox","hbf","dialog");
   zdialog_add_widget(zd,"button","files","hbf",TX("Select Files"),"space=5");
   zdialog_add_widget(zd,"label","fcount","hbf",TX("no files selected"),"space=10");
   zdialog_add_widget(zd,"hbox","hbloc","dialog");
   zdialog_add_widget(zd,"label","labloc","hbloc",TX("To Location"),"space=5");
   zdialog_add_widget(zd,"zentry","toloc","hbloc",0,"expand");
   zdialog_add_widget(zd,"button","browse","hbloc",TX("Browse"),"space=5");
   zdialog_add_widget(zd,"hbox","hbwh","dialog");
   zdialog_add_widget(zd,"label","labw","hbwh",TX("max. Width"),"space=5");
   zdialog_add_widget(zd,"zentry","maxww","hbwh","1000","size=5");
   zdialog_add_widget(zd,"label","space","hbwh",0,"space=5");
   zdialog_add_widget(zd,"label","labh","hbwh",TX("Height"),"space=5");
   zdialog_add_widget(zd,"zentry","maxhh","hbwh","700","size=5");
   zdialog_add_widget(zd,"check","samesize","hbwh","no change","space=12");
   zdialog_add_widget(zd,"hbox","hbmeta","dialog");
   zdialog_add_widget(zd,"check","meta","hbmeta","export metadata");

   zdialog_load_inputs(zd);                                                            //  preload prior location

   snprintf(text,100,TX("%d image files selected"),SFcount);                           //  show selected files count
   zdialog_stuff(zd,"fcount",text);

   zdialog_resize(zd,400,0);
   zdialog_run(zd,export_files_dialog_event,"parent");                                 //  run dialog
   zstat = zdialog_wait(zd);                                                           //  wait for completion
   zdialog_free(zd);

   if (zstat != 1) {
      Fblock(0);
      return;
   }

   free_resources();                                                                   //  no current file

   zdpop = popup_report_open(TX("exporting files"),Mwin,600,400,0,0,0,"X",null);       //  log report

   zadd_locked(NFbusy,+1);

   for (ii = 0; ii < SFcount; ii++)                                                    //  loop selected files
   {
      zmainloop();                                                                     //  keep GTK awake

      if (! zdialog_valid(zdpop)) break;                                               //  canceled

      infile = SelFiles[ii];                                                           //  input filespec
      popup_report_write2(zdpop,0,"%s \n",infile);

      pp = strrchr(infile,'/');                                                        //  construct output filespec
      if (! pp) continue;
      cc = strlen(pp) + 8;
      outfile = zstrdup(tolocation,"export files",cc);
      strcat(outfile,pp);
      pp = strrchr(outfile,'.');                                                       //  outfile is type .jpg
      if (! pp) continue;
      strcpy(pp,".jpg");

      pxmin = PXM_load(infile,0);                                                      //  read input file
      if (! pxmin) {
         popup_report_write2(zdpop,1,TX("*** file type not supported \n"));
         zfree(outfile);
         continue;
      }

      ww = pxmin->ww;                                                                  //  input file size
      hh = pxmin->hh;

      if (Fsamesize) pxmout = pxmin;                                                   //  same size, output = input
      else {
         wscale = hscale = 1.0;
         if (ww > maxww) wscale = 1.0 * maxww / ww;                                    //  compute new size
         if (hh > maxhh) hscale = 1.0 * maxhh / hh;
         if (wscale < hscale) scale = wscale;
         else scale = hscale;
         if (scale > 0.999) pxmout = pxmin;                                            //  no change
         else {
            ww = ww * scale;
            hh = hh * scale;
            pxmout = PXM_rescale(pxmin,ww,hh);                                         //  rescaled output file
            PXM_free(pxmin);                                                           //  free memory
         }
      }

      err = PXM_JPG_save(pxmout,outfile,jpeg_def_quality);                             //  write output file
      if (err) popup_report_write2(zdpop,1,TX("*** cannot create new file \n"));

      if (! err && Fmeta) {                                                            //  optional copy metadata
         err = meta_get(infile,(ch **) tagnames,tagvals,NK);
         if (! err) {
            err = meta_put(outfile,(ch **) tagnames,tagvals,NK);
            if (err) popup_report_write2(zdpop,1,TX("*** metadata update error \n"));
         }
      }

      PXM_free(pxmout);
      zfree(outfile);
   }

   if (! zdialog_valid(zdpop))
      printf("*** report canceled \n");
   else popup_report_write2(zdpop,0,TX("\n *** COMPLETED \n"));

   popup_report_bottom(zdpop);

   zadd_locked(NFbusy,-1);

   Fblock(0);
   return;
}


//  dialog event and completion callback function

int export_files_dialog_event(zdialog *zd, ch *event)
{
   using namespace export_files_names;

   int      cc;
   ch       countmess[80];
   ch       *ploc;

   if (strmatch(event,"files"))                                                        //  select files to export
   {
      select_files(0);                                                                 //  get list of files to export
      snprintf(countmess,80,TX("%d image files selected"),SFcount);                    //  update dialog
      zdialog_stuff(zd,"fcount",countmess);
   }

   if (strmatch(event,"browse")) {
      zdialog_fetch(zd,"toloc",tolocation,XFCC);
      ploc = zgetfile(TX("Select folder"),MWIN,"folder",tolocation);                   //  new location browse
      if (! ploc) return 1;
      zdialog_stuff(zd,"toloc",ploc);
      zfree(ploc);
   }

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

   if (! SFcount) {                                                                    //  [proceed]
      zmessageACK(Mwin,TX("no files selected"));                                       //  no files selected
      zd->zstat = 0;                                                                   //  keep dialog active
      return 1;
   }

   zdialog_fetch(zd,"toloc",tolocation,XFCC);                                          //  get output location
   if (! dirfile(tolocation)) {
      zmessageACK(Mwin,TX("location is not a folder"));
      zd->zstat = 0;
   }

   cc = strlen(tolocation) - 1;                                                        //  remove trailing '/' if present
   if (tolocation[cc] == '/') tolocation[cc] = 0;

   zdialog_fetch(zd,"samesize",Fsamesize);                                             //  get rescale options
   zdialog_fetch(zd,"maxww",maxww);
   zdialog_fetch(zd,"maxhh",maxhh);
   zdialog_fetch(zd,"meta",Fmeta);                                                     //  metadata option

   return 1;
}


