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

   Fotocx - edit photos and manage collections

   Copyright 2007-2024 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 functions - Mashup and Array functions

   m_mashup          combine images and text into custom layout
   m_image_array     combine images into a array or matrix of images

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

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

using namespace zfuncs;

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

   MASHUP

   Define a layout/background image.
   Add images, text, and lines as overlays on top of layout.
   Overlays have adjustable position, angle, size, transparency.

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

namespace mashup
{
   ch          projname[100];                                                    //  project name
   ch          projfolder[200];                                                  //  <mashup_folder>/projname
   ch          masterfile[200];                                                  //  <projfolder>/masterdata
   ch          tranfile[200];                                                    //  <projfolder>/trandata
   ch          warpfile[200];                                                    //  <projfolder>/warpdata
   ch          layoutfile[XFCC];                                                 //  background/layout image file
   int         Lww, Lhh;                                                         //  layout image size
   int         flatRGB[3];                                                       //  flat layout RGB values (no file)
   PXB         *Lpxb;                                                            //  layout image PXB
   int         Nimage;                                                           //  overlay image count
   int         Ntext;                                                            //  overlay text count
   int         Nline;                                                            //  overlay line count
   int         updxlo, updxhi, updylo, updyhi;                                   //  update region in layout image
   float       PRscale;                                                          //  project scale for editing
   int         Fupdall;                                                          //  flag, update entire layout

   ch          *focus;                                                           //  "image" "text" "line" ""
   int         focusii;                                                          //  image[ii] text[ii] line[ii] or -1
   int         Fnextimage = 0;                                                   //  flag, image selected with [next]

   int         Mradius = 50;                                                     //  paint transparency mouse radius
   int         Mpowcen = 10;                                                     //  paint transparency mouse center power
   int         Mpowedge = 0;                                                     //  paint transparency mouse edge power
   int         Mgradual = 1;                                                     //  paint transparency gradual or instant

   zdialog     *zdimage;                                                         //  image edit dialog active
   zdialog     *zdtext;                                                          //  text edit dialog active
   zdialog     *zdaddtext;                                                       //  add text dialog active
   zdialog     *zdline;                                                          //  line edit dialog active
   zdialog     *zdaddline;                                                       //  add line dialog active
   zdialog     *zdtransp;                                                        //  paint transparencies dialog active
   zdialog     *zdwarp;                                                          //  warp image dialog active

   struct image_t {                                                              //  overlay image data
      ch       *file;                                                            //  filespec
      PXB      *pxb1;                                                            //  image PXB, 1x size
      PXB      *pxb2;                                                            //  image PXB, scaled size
      float    scale;                                                            //  pxb2 scale, 0.0 to 1.0 = 1x
      float    theta;                                                            //  angle, -PI to +PI radians
      float    sinT, cosT;                                                       //  trig values for theta
      int      ww1, hh1;                                                         //  image size at 1x scale
      int      ww2, hh2;                                                         //  image size at curr. scale
      int      px0, py0;                                                         //  image position in layout image
      int      pxlo, pxhi, pylo, pyhi;                                           //  image extent in layout image space
      int      fixed;                                                            //  flag, image fixed, cannot move
      int      Lmarg, Rmarg, Tmarg, Bmarg;                                       //  left/right/top/bottom hard margins
      int      Lblend, Rblend, Tblend, Bblend;                                   //  left/right/top/bottom blended margins
      float    Btransp;                                                          //  base image transparency 0...1
      int      vtrancc;                                                          //  var. transparency map cc
      uint8    *vtranmap;                                                        //  var. transparency map (pxb1 image)
      int      Nwarp;                                                            //  warpmem count
      float    warpmem[200][5];                                                  //  warp undo memory, last 200 warps
      int      warpcc;                                                           //  warpx/warpy data length
      float    *warpx, *warpy;                                                   //  pixel warps (virt. source pixels)
   };

   struct text_t {                                                               //  overlay text data
      textattr_t  attr;                                                          //  text, font, angle, outline ...
      int         ww, hh;                                                        //  image dimensions in layout image
      int         px0, py0;                                                      //  image position in layout image
      int         pxlo, pxhi, pylo, pyhi;                                        //  extent of text image in layout image
   };

   struct line_t {                                                               //  overlay line/arrow data
      lineattr_t  attr;                                                          //  attributes (length, width, arrowheads)
      int         ww, hh;                                                        //  image dimensions in layout image
      int         px0, py0;                                                      //  image position in layout image
      int         pxlo, pxhi, pylo, pyhi;                                        //  extent of line image in layout image
   };

   #define  maxmash 100                                                          //  max. images, text, lines
   image_t     image[maxmash];
   text_t      text[maxmash];
   line_t      line[maxmash];

   void  project(void);
   int   project_dialog_event(zdialog *zd, ch *event);
   int   project_open(void);
   void  project_save(void);
   void  project_rescale(float scale);

   void  image_edit(void);
   int   image_dialog_event(zdialog *zd, ch *event);
   void  image_rescale(int ii, float scale2);
   void  paintransp_dialog(void);
   int   paintransp_dialog_event(zdialog *zd, ch *event);
   void  paintransp_mousefunc(void);
   void  warp_dialog(void);
   int   warp_dialog_event(zdialog *zd, ch *event);
   void  warp_mousefunc(void);
   void  warp_warpfunc(void);
   void  * warp_warpfunc_wthread(void *arg);
   void  transparent_margins(void);
   void  add_image(void);
   void  remove_image(int ii);
   void  flashimage(void);

   void  text_edit(void);
   int   text_dialog_event(zdialog *zd, ch *event);
   void  add_text(void);
   void  remove_text(int ii);
   void  flashtext(void);

   void  line_edit(void);
   int   line_dialog_event(zdialog *zd, ch *event);
   void  add_line(void);
   void  remove_line(int ii);
   void  flashline(void);

   void  select(ch *which, int ii);
   void  setlayoutupdatearea(void);
   void  mousefunc_layout(void);
   void  KBfunc(int key);
   void  Lupdate(int = 0);
   void  * Lupdate_wthread(void *arg);
}


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

//  menu function

void m_mashup(GtkWidget *, ch *menu)
{
   using namespace mashup;

   zdialog     *zd;
   int         zstat, nn, err, ww, hh;
   ch          color[20], *file;
   ch          *choice = 0, *ppc;
   int         px, py;
   uint8       *pix;

   F1_help_topic = "mashup";

   Plog(1,"m_mashup \n");

   if (Fblock("mashup")) return;

   *projname = *layoutfile = *tranfile = *warpfile = 0;                          //  clear project data
   zdimage = zdtransp = zdwarp = zdtext = zdline = 0;
   Nimage = Ntext = Nline = 0;

   for (nn = 0; nn < maxmash; nn++) {
      memset(&image[nn],0,sizeof(image_t));
      memset(&text[nn],0,sizeof(text_t));
      memset(&line[nn],0,sizeof(line_t));
   }

   select("",-1);                                                                //  nothing selected

/***
       _________________________________________
      |                                         |
      |  Project name [_______________________] |
      |                                         |
      |  Layout and background image            |
      |  (o) choose an image file               |
      |  (o) use the current image file         |
      |  (o) specify background size and color  |
      |  (o) use a saved Mashup project         |
      |                                         |
      |                           [Proceed] [X] |
      |_________________________________________|

***/

   zd = zdialog_new("Mashup",Mwin,"Proceed"," X ",null);
   zdialog_add_widget(zd,"hbox","hbpn","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labpn","hbpn","Project name","space=3");
   zdialog_add_widget(zd,"zentry","projname","hbpn","mashup","space=3|expand");
   zdialog_add_widget(zd,"hbox","space","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbl","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labtitle","hbl","Layout and background image","space=3");
   zdialog_add_widget(zd,"hbox","hbopt","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","space","hbopt",0,"space=5");
   zdialog_add_widget(zd,"vbox","vbopt","hbopt");
   zdialog_add_widget(zd,"radio","choosefile","vbopt","choose an image file");
   zdialog_add_widget(zd,"radio","usecurrent","vbopt","use the current image file");
   zdialog_add_widget(zd,"radio","makelayout","vbopt","specify background size and color");
   zdialog_add_widget(zd,"radio","openproject","vbopt","use a saved Mashup project");

   zdialog_stuff(zd,"choosefile",1);

   zdialog_run(zd,0,"save");                                                     //  run dialog, wait for completion
   zstat = zdialog_wait(zd);
   if (zstat != 1) {                                                             //  canceled
      zdialog_free(zd);
      Fblock(0);
      return;
   }

   zdialog_fetch(zd,"projname",projname,100);                                    //  get project name
   strTrim2(projname);

   if (*projname <= ' ') {
      zmessageACK(Mwin,"enter a project name");
      zdialog_free(zd);
      goto try_again;
   }

   if (strchr(projname,'"') || strchr(projname,'\'')) {
      zmessageACK(Mwin,"project name cannot have quote characters");
      zdialog_free(zd);
      goto try_again;
   }

   zdialog_fetch(zd,"choosefile",nn);
   if (nn) choice = "choosefile";
   zdialog_fetch(zd,"usecurrent",nn);
   if (nn) choice = "usecurrent";
   zdialog_fetch(zd,"makelayout",nn);
   if (nn) choice = "makelayout";
   zdialog_fetch(zd,"openproject",nn);
   if (nn) choice = "openproject";

   zdialog_free(zd);

   if (strmatch(choice,"choosefile"))                                            //  open new curr. file >> layout
   {
      file = select_files1(curr_file);
      if (! file) goto try_again;
      Lpxb = PXB_load(file,1);                                                   //  layout image
      if (! Lpxb) goto try_again;
      Lww = Lpxb->ww;                                                            //  layout size
      Lhh = Lpxb->hh;
      strncpy0(layoutfile,file,XFCC);

      project();                                                                 //  start project
      return;
   }

   if (strmatch(choice,"usecurrent")) {                                          //  curr. file >> layout
      if (! curr_file) {                                                         //  bugfix                                24.60
         zmessageACK(Mwin,"no current file");
         goto try_again;
      }
      file = zstrdup(curr_file,"mashup");
      Lpxb = PXB_load(file,1);                                                   //  layout image
      if (! Lpxb) goto try_again;
      Lww = Lpxb->ww;                                                            //  layout size
      Lhh = Lpxb->hh;
      strncpy0(layoutfile,file,XFCC);

      project();                                                                 //  start project
      return;
   }

   if (strmatch(choice,"makelayout"))                                            //  create flat layout image
   {
      /***
                _______________________________________________
               |        Make Layout Image                      |
               |                                               |
               |  width [____]  height [____] (pixels)         |
               |  color [____]                                 |
               |                                      [OK] [X] |
               |_______________________________________________|

      ***/

      zd = zdialog_new("Make Layout Image",Mwin,"OK"," X ",null);
      zdialog_add_widget(zd,"hbox","hbz","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labw","hbz","Width","space=5");
      zdialog_add_widget(zd,"zspin","width","hbz","1000|40000|1|2000");          //  limit 40K x 40K = 1600 megapixels 
      zdialog_add_widget(zd,"label","space","hbz",0,"space=5");
      zdialog_add_widget(zd,"label","labh","hbz","Height","space=5");
      zdialog_add_widget(zd,"zspin","height","hbz","600|40000|1|1200");
      zdialog_add_widget(zd,"label","labp","hbz","(pixels)","space=3");
      zdialog_add_widget(zd,"hbox","hbc","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labc","hbc","Color","space=5");
      zdialog_add_widget(zd,"colorbutt","color","hbc","200|200|200");

      zdialog_load_inputs(zd);                                                   //  preload prior user inputs

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

      if (zstat != 1) {                                                          //  cancel
         zdialog_free(zd);
         goto try_again;
      }

      zdialog_fetch(zd,"width",ww);                                              //  get layout dimensions
      zdialog_fetch(zd,"height",hh);

      flatRGB[0] = flatRGB[1] = flatRGB[2] = 255;
      zdialog_fetch(zd,"color",color,19);                                        //  get flat layout color
      ppc = substring(color,"|",1);
      if (ppc) flatRGB[0] = atoi(ppc);
      ppc = substring(color,"|",2);
      if (ppc) flatRGB[1] = atoi(ppc);
      ppc = substring(color,"|",3);
      if (ppc) flatRGB[2] = atoi(ppc);

      zdialog_free(zd);

      Lpxb = PXB_make(ww,hh,3);                                                  //  layout image

      Lww = Lpxb->ww;                                                            //  layout size
      Lhh = Lpxb->hh;
      *layoutfile = 0;                                                           //  no layout file (flat layout)

      for (py = 0; py < Lhh; py++)                                               //  fill layout with flat color
      for (px = 0; px < Lww; px++) {
         pix = PXBpix(Lpxb,px,py);
         pix[0] = flatRGB[0];
         pix[1] = flatRGB[1];
         pix[2] = flatRGB[2];
      }

      project();                                                                 //  start project
      return;
   }

   if (strmatch(choice,"openproject")) {                                         //  project file >> layout & contents
      err = project_open();
      if (err) goto try_again;
      project();                                                                 //  start project
      return;
   }

try_again:                                                                       //  restart mashup
   Fblock(0);
   m_mashup(0,0);
   return;
}


//  overall steering dialog - edit dialogs return to this dialog

void mashup::project()
{
   using namespace mashup;

   zdialog     *zd;

   free_resources();                                                             //  no E1/E3 etc.
   Fmashup = 1;                                                                  //  flag for KBpress() func

   Fpxb = PXB_copy(Lpxb);                                                        //  initz. window update image
   viewmode('F');                                                                //  force file view

   PRscale = 1.0;
   select("",-1);                                                                //  nothing selected
   Fupdall = 1;
   Lupdate(1);                                                                   //  update layout composite image

/***
       ____________________________________________
      |          Mashup                            |
      |                                            |
      |  [Edit Images]   add or edit images        |
      |  [Edit Text]     add or edit text          |
      |  [Edit Line]     add or edit lines/arrows  |
      |  [Rescale]       change project scale      |
      |  [OK]            project complete          |
      |  [X]             cancel project            |
      |____________________________________________|

***/

   zd = zdialog_new("Mashup",Mwin,null);

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

   zdialog_add_widget(zd,"button","editimage","vb1","Edit Images","space=3");
   zdialog_add_widget(zd,"button","edittext","vb1","Edit Text","space=3");
   zdialog_add_widget(zd,"button","editline","vb1","Edit Line","space=3");
   zdialog_add_widget(zd,"button","rescale","vb1","Rescale","space=3");
   zdialog_add_widget(zd,"button","done","vb1","OK","space=3");
   zdialog_add_widget(zd,"button","cancel","vb1"," X ","space=3");

   zdialog_add_widget(zd,"hbox","hbedim","vb2");
   zdialog_add_widget(zd,"label","labedim","hbedim","add or edit images","space=3");
   zdialog_add_widget(zd,"hbox","hbedtx","vb2");
   zdialog_add_widget(zd,"label","labedtx","hbedtx","add or edit text","space=3");
   zdialog_add_widget(zd,"hbox","hbedln","vb2");
   zdialog_add_widget(zd,"label","labedln","hbedln","add or edit lines/arrows","space=3");
   zdialog_add_widget(zd,"hbox","hbres","vb2");
   zdialog_add_widget(zd,"label","labscale","hbres","change project scale","space=3");
   zdialog_add_widget(zd,"hbox","hbdone","vb2");
   zdialog_add_widget(zd,"label","labdone","hbdone","project complete","space=3");
   zdialog_add_widget(zd,"hbox","hbcan","vb2");
   zdialog_add_widget(zd,"label","labcancel","hbcan","cancel project","space=3");

   zdialog_run(zd,project_dialog_event,"save");                                  //  start dialog
   takeMouse(mousefunc_layout,0);                                                //  capture mouse click/drag

   return;
}


//  project dialog event and completion function

int mashup::project_dialog_event(zdialog *zd, ch *event)
{
   using namespace mashup;

   int      ii, cc, yn;
   GError   *gerror = 0;
   ch       *file = 0, *outfile, *pp;
   ch       *rescalemess = "rescale project";
   STATB    statB;

   if (zd->zstat) event = "cancel";

   if (strmatch(event,"focus")) {
      takeMouse(mousefunc_layout,0);
      return 1;
   }

   if (strmatch(event,"editimage")) {
      zdialog_show(zd,0);
      image_edit();                                                              //  start edit images dialog
      zdialog_show(zd,1);
      return 1;
   }

   if (strmatch(event,"edittext")) {
      zdialog_show(zd,0);
      text_edit();                                                               //  start edit text dialog
      zdialog_show(zd,1);
      return 1;
   }

   if (strmatch(event,"editline")) {
      zdialog_show(zd,0);
      line_edit();                                                               //  start edit line dialog
      zdialog_show(zd,1);
      return 1;
   }

   if (strmatch(event,"rescale")) {                                              //  rescale project bigger, or reset
      ii = zdialog_choose(Mwin,"mouse",rescalemess,"2x","3x","4x","Reset",null);
      if (ii == 1 && PRscale == 1.0) project_rescale(2.0);                       //  1x -> 2x
      if (ii == 2 && PRscale == 1.0) project_rescale(3.0);                       //  1x -> 3x
      if (ii == 3 && PRscale == 1.0) project_rescale(4.0);                       //  1x -> 4x
      if (ii == 4 && PRscale > 1.0) project_rescale(1.0);                        //  [reset] back to 1x
      return 1;
   }

   if (strmatch(event,"done"))                                                   //  project done
   {
      zdialog_free(zd);                                                          //  kill dialog
      freeMouse();

      if (navi::gallerytype == FOLDER) {                                         //  default output image file name
         outfile = zstrdup(navi::galleryname,"mashup",108);
         cc = strlen(outfile);
         strncatv(outfile,cc+108,"/",projname,".png",null);                      //  gallery/projname.png
      }
      else {
         outfile = zstrdup(projname,"mashup",6);                                 //  projname.png
         strcat(outfile,".png");
      }

      file = zgetfile("save Mashup output file",MWIN,"save",outfile);            //  ask user for file name
      zfree(outfile);
      if (file) {
         pp = strrchr(file,'.');                                                 //  force .png extension
         if (! pp || strlen(pp) < 4) {
            pp = zstrdup(file,"mashup",6);
            zfree(file);
            file = pp;
            pp = strrchr(file,'.');
            if (! pp || strlen(pp) > 5) pp = file + strlen(file);
         }
         strcpy(pp,".png");
         ii = gdk_pixbuf_save(Fpxb->pixbuf,file,"png",&gerror,"compression","1",null);
         if (! ii) {
            zmessageACK(Mwin,gerror->message);
            zfree(file);
            file = 0;
         }
      }

      yn = zmessageYN(Mwin,"save Mashup project?");                              //  offer to save project
      if (yn) project_save();

      event = "cleanup";
   }

   if (strmatch(event,"cancel"))                                                 //  project canceled
   {
      Fescape = 0;                                                               //  user kill 

      yn = zmessageYN(Mwin,"cancel project?");
      if (! yn) return 1;

      zdialog_free(zd);                                                          //  kill dialog
      freeMouse();

      if (dirfile(projfolder,&statB)) {
         yn = zmessageYN(Mwin,"delete Mashup project files?");                   //  offer to delete project files
         if (yn) zshell("log","rm -f -R %s",projfolder);
      }

      viewmode('G');
      event = "cleanup";
   }

   if (strmatch(event,"cleanup"))
   {
      PXB_free(Lpxb);
      Lpxb = 0;

      Fmashup = 0;

      if (file) {                                                                //  open finished mashup
         f_open(file,0,0,1);
         zfree(file);
      }
      else free_resources(0);                                                    //  leave a blank window

      Fblock(0);
   }

   return 1;
}


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

//  open and restore a saved mashup project
//  return 0 if success, +N if error

int mashup::project_open()
{
   using namespace mashup;

   PXB         *pxbtemp;
   int         ii, nn;
   int         ww1, hh1;
   int         px, py, kk;
   int         nfid = -1, cc, wcc, vcc;
   ch          *pp, *pp2;
   ch          buff[XFCC];
   textattr_t  *txattr = 0;
   lineattr_t  *lnattr = 0;
   FILE        *fid;
   uint8       *pix;

   pp = zgetfile("mashup project",MWIN,"folder",mashup_folder);                  //  get project folder
   if (! pp) return 1;
   snprintf(projfolder,200,"%s",pp);                                             //  project folder
   zfree(pp);
   snprintf(masterfile,200,"%s/masterdata",projfolder);                          //  project master data file
   snprintf(tranfile,200,"%s/trandata",projfolder);                              //  transparency data file
   snprintf(warpfile,200,"%s/warpdata",projfolder);                              //  warp data file

   pp = strrchr(projfolder,'/');                                                 //  project name from folder name
   strncpy0(projname,pp+1,100);

   //  read project file and validate all data

   fid = fopen(masterfile,"r");                                                  //  open master file
   if (! fid) goto failure;

   pp = fgets_trim(buff,XFCC,fid);                                               //  read layout record
   if (! pp) { Plog(0,"no layout \n"); goto badproject; }

   if (strmatchN(pp,"layout:",7))                                                //  layout: /.../filename.jpg
   {
      pp += 7;
      while (*pp == ' ') pp++;

      strncpy0(layoutfile,pp,XFCC);
      if (! regfile(layoutfile)) {
         zmessageACK(Mwin,"layout image file missing: \n %s",layoutfile);
         Plog(0,"no layout image \n");
         goto badproject;
      }

      Lpxb = PXB_load(layoutfile,1);                                             //  layout image
      if (! Lpxb) { Plog(0,"PXB_load() layout file \n"); goto badproject; }

      Lww = Lpxb->ww;                                                            //  layout size
      Lhh = Lpxb->hh;
   }

   else if (strmatchN(pp,"flatlayout:",11))                                      //  flatlayout: ww hh R|G|B
   {
      nn = sscanf(buff,"flatlayout: %d %d RGB: %d/%d/%d \n",
                   &ww1,&hh1,&flatRGB[0],&flatRGB[1],&flatRGB[2]);
      if (nn != 5) { Plog(0,"flatlayout rec. \n"); goto badproject; }

      *layoutfile = 0;                                                           //  flat layout, no layout file

      Lpxb = PXB_make(ww1,hh1,3);                                                //  layout image

      Lww = Lpxb->ww;                                                            //  layout size
      Lhh = Lpxb->hh;

      for (py = 0; py < Lhh; py++)                                               //  fill layout with flat color
      for (px = 0; px < Lww; px++) {
         pix = PXBpix(Lpxb,px,py);
         pix[0] = flatRGB[0];
         pix[1] = flatRGB[1];
         pix[2] = flatRGB[2];
      }
   }

   else { Plog(0,"unknown rec. \n"); goto badproject; }

   //  read any number of overlay image records

   while (true)
   {
      pp = fgets_trim(buff,XFCC,fid);
      if (! pp) break;
      if (strmatchN(pp,"end",3)) break;

      if (strmatchN(pp,"image:",6))                                              //  image: /.../filename.jpg
      {
         pp += 6;
         while (*pp == ' ') pp++;
         if (! regfile(pp)) {
            zmessageACK(Mwin,"overlay image file missing: \n %s",pp);
            continue;
         }

         ii = Nimage++;
         cc = sizeof(image_t);                                                   //  clear image data
         memset(&image[ii],0,cc);

         image[ii].file = zstrdup(pp,"mashup");                                  //  overlay image file

         pxbtemp = PXB_load(image[ii].file,1);                                   //  make overlay image
         if (! pxbtemp) goto cleanupErr;
         PXB_addalpha(pxbtemp);                                                  //  add alpha channel if missing

         image[ii].pxb1 = pxbtemp;
         image[ii].ww1 = pxbtemp->ww;
         image[ii].hh1 = pxbtemp->hh;

         continue;
      }

      ii = Nimage - 1;                                                           //  current image

      if (strmatchN(pp,"position:",9)) {                                         //  position: xxx yyy
         nn = sscanf(pp,"position: %d %d",&image[ii].px0,&image[ii].py0);
         if (nn != 2) { Plog(0,"position rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"fixed:",6)) {                                            //  position fixed: n
         nn = sscanf(pp,"fixed: %d",&image[ii].fixed);
         if (nn != 1) { Plog(0,"fixed rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"scale:",6)) {                                            //  scale: n.nnn
         nn = sscanf(pp,"scale: %f",&image[ii].scale);
         if (nn != 1) { Plog(0,"scale rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"theta:",6)) {                                            //  theta: n.nnn
         nn = sscanf(pp,"theta: %f",&image[ii].theta);
         if (nn != 1) { Plog(0,"theta rec. \n"); goto badproject; }
         image[ii].sinT = sin(image[ii].theta);
         image[ii].cosT = cos(image[ii].theta);
         continue;
      }

      if (strmatchN(pp,"Btransp:",8)) {                                          //  Btransp: 0.nnn
         nn = sscanf(pp,"Btransp: %f",&image[ii].Btransp);
         if (nn != 1) { Plog(0,"Btransp rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"Lmarg:",6)) {                                            //  Lmarg: nnn
         nn = sscanf(pp,"Lmarg: %d",&image[ii].Lmarg);
         if (nn != 1) { Plog(0,"Lmarg rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"Rmarg:",6)) {                                            //  Rmarg: nnn
         nn = sscanf(pp,"Rmarg: %d",&image[ii].Rmarg);
         if (nn != 1) { Plog(0,"Rmarg rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"Tmarg:",6)) {                                            //  Tmarg: nnn
         nn = sscanf(pp,"Tmarg: %d",&image[ii].Tmarg);
         if (nn != 1) { Plog(0,"Tmarg rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"Bmarg:",6)) {                                            //  Bmarg: nnn
         nn = sscanf(pp,"Bmarg: %d",&image[ii].Bmarg);
         if (nn != 1) { Plog(0,"Bmarg rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"Lblend:",7)) {                                           //  Lblend: nnn
         nn = sscanf(pp,"Lblend: %d",&image[ii].Lblend);
         if (nn != 1) { Plog(0,"Lblend rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"Rblend:",7)) {                                           //  Rblend: nnn
         nn = sscanf(pp,"Rblend: %d",&image[ii].Rblend);
         if (nn != 1) { Plog(0,"Rblend rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"Tblend:",7)) {                                           //  Tblend: nnn
         nn = sscanf(pp,"Tblend: %d",&image[ii].Tblend);
         if (nn != 1) { Plog(0,"Tblend rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"Bblend:",7)) {                                           //  Bblend: nnn
         nn = sscanf(pp,"Bblend: %d",&image[ii].Bblend);
         if (nn != 1) { Plog(0,"Bblend rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"Nwarp:",6)) {                                            //  Nwarp: nn
         nn = sscanf(pp,"Nwarp: %d",&image[ii].Nwarp);
         if (nn != 1) { Plog(0,"Nwarp rec. \n"); goto badproject; }
         if (image[ii].Nwarp > 200) { Plog(0,"Nwarp rec. \n"); goto badproject; }
         for (kk = 0; kk < image[ii].Nwarp; kk++) {
            pp = fgets_trim(buff,XFCC,fid);                                      //  n.nnn n.nnn n.nnn n.nnn n.nnn
            if (! pp) break;                                                     //  (loop for Nwarp records)
            nn = sscanf(pp,"%f %f %f %f %f",&image[ii].warpmem[kk][0],
                  &image[ii].warpmem[kk][1],&image[ii].warpmem[kk][2],
                  &image[ii].warpmem[kk][3],&image[ii].warpmem[kk][4]);
            if (nn != 5) { Plog(0,"warpmem rec. \n"); goto badproject; }
         }
      }

      ww1 = image[ii].ww1;
      hh1 = image[ii].hh1;

      if (strmatchN(pp,"vtrancc:",8)) {                                          //  vtrancc: nnnn
         nn = sscanf(pp,"vtrancc: %d",&vcc);
         if (nn != 1) { Plog(0,"vtrancc rec. \n"); goto badproject; }
         if (vcc && vcc != ww1 * hh1) { Plog(0,"vtrancc rec. \n"); goto badproject; }
         image[ii].vtrancc = vcc;
         continue;
      }

      if (strmatchN(pp,"warpcc:",7)) {                                           //  warpcc: nnnn
         nn = sscanf(pp,"warpcc: %d",&wcc);
         if (nn != 1) { Plog(0,"warpcc rec. \n"); goto badproject; }
         if (wcc && wcc != (int) (ww1 * hh1 * sizeof(float))) { Plog(0,"warpcc rec. \n"); goto badproject; }
         image[ii].warpcc = wcc;
         continue;
      }
   }

   //  read any number of text records

   while (true)
   {
      pp = fgets_trim(buff,XFCC,fid);
      if (! pp) break;
      if (strmatchN(pp,"end",3)) break;

      if (strmatchN(pp,"text:",5))                                               //  text: this is my text ...
      {
         ii = Ntext++;
         txattr = &text[ii].attr;
         pp += 5;
         while (*pp == ' ') pp++;
         repl_Nstrs(pp,txattr->text,999,"\\n","\n",null);                        //  replace "\n" with newline
         continue;
      }

      ii = Ntext - 1;                                                            //  current text

      if (strmatchN(pp,"position:",9)) {                                         //  position: xx yy
         nn = sscanf(pp,"position: %d %d",&text[ii].px0,&text[ii].py0);
         if (nn != 2) { Plog(0,"position rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"font:",5)) {                                             //  font: free sans  size: nn
         pp += 5;
         while (*pp == ' ') pp++;
         pp2 = strstr(pp,"size:");
         if (! pp2) { Plog(0,"font rec. \n"); goto badproject; }
         *pp2 = 0;
         pp2 += 5;
         strncpy0(txattr->font,pp,80);
         txattr->size = atoi(pp2);
         if (txattr->size < 8) { Plog(0,"font rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"attributes:",11)) {                                      //  attributes
         txattr = &text[ii].attr;
         nn = sscanf(pp,"attributes: %f  %s %s %s %s  %d %d %d %d  %d %d %d",
                  &txattr->angle, txattr->color[0], txattr->color[1], txattr->color[2], txattr->color[3],
                  &txattr->transp[0], &txattr->transp[1], &txattr->transp[2], &txattr->transp[3],
                  &txattr->towidth, &txattr->shwidth, &txattr->shangle);
         if (nn != 12) { Plog(0,"attributes rec. \n"); goto badproject; }
      }
   }

   //  read any number of line/arrow records

   while (true)
   {
      pp = fgets_trim(buff,XFCC,fid);
      if (! pp) break;
      if (strmatchN(pp,"end",3)) break;

      if (strmatchN(pp,"line:",5)) {                                             //  line:
         ii = Nline++;
         continue;
      }

      ii = Nline - 1;                                                            //  current line

      if (strmatchN(pp,"position:",9)) {                                         //  position: xx yy
         nn = sscanf(pp,"position: %d %d",&line[ii].px0,&line[ii].py0);
         if (nn != 2) { Plog(0,"position rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"attributes:",11)) {                                      //  attributes:
         lnattr = &line[ii].attr;
         nn = sscanf(pp,"attributes: %d %d %d %d %f  %s %s %s %s  %d %d %d %d  %d %d %d",
                  &lnattr->length, &lnattr->width, &lnattr->larrow, &lnattr->rarrow, &lnattr->angle,
                  lnattr->color[0], lnattr->color[1], lnattr->color[2], lnattr->color[3],
                  &lnattr->transp[0], &lnattr->transp[1], &lnattr->transp[2], &lnattr->transp[3],
                  &lnattr->towidth, &lnattr->shwidth, &lnattr->shangle);
         if (nn != 16) { Plog(0,"attributes rec. \n"); goto badproject; }
      }
   }

   fclose(fid);
   fid = 0;

   //  read binary transparency file if any

   for (vcc = ii = 0; ii < Nimage; ii++)                                         //  sum transparency data
      vcc += image[ii].vtrancc;

   if (vcc)
   {
      nfid = open(tranfile,O_RDONLY);
      if (nfid < 0) goto failure;

      for (ii = 0; ii < Nimage; ii++)                                            //  read transparency data
      {
         vcc = image[ii].vtrancc;
         if (vcc) {
            image[ii].vtranmap = (uint8 *) zmalloc(vcc,"mashup");
            cc = read(nfid,image[ii].vtranmap,vcc);
            if (cc != vcc) { Plog(0,"vtranmap data \n"); goto badproject; }
         }
         else image[ii].vtranmap = 0;
      }

      close(nfid);
      nfid = -1;
   }

   //  read binary warp data file if any

   for (wcc = ii = 0; ii < Nimage; ii++)                                         //  sum warp data
      wcc += image[ii].warpcc;

   if (wcc)
   {
      nfid = open(warpfile,O_RDONLY);
      if (nfid < 0) goto failure;

      for (ii = 0; ii < Nimage; ii++)                                            //  read warp data
      {
         wcc = image[ii].warpcc;
         if (wcc) {
            image[ii].warpx = (float *) zmalloc(wcc,"mashup");
            cc = read(nfid,image[ii].warpx,wcc);
            if (cc != wcc) { Plog(0,"warpx data \n"); goto badproject; }
            image[ii].warpy = (float *) zmalloc(wcc,"mashup");
            cc = read(nfid,image[ii].warpy,wcc);
            if (cc != wcc) { Plog(0,"warpy data \n"); goto badproject; }
         }
         else image[ii].warpx = image[ii].warpy = 0;
      }

      close(nfid);
      nfid = -1;
   }

   //  generate overlay images

   for (ii = 0; ii < Nimage; ii++)
   {
      image_rescale(ii,image[ii].scale);                                         //  scaled and warped image
      select("image",ii);
      setlayoutupdatearea();                                                     //  set layout update area
   }

   //  generate text images

   for (ii = 0; ii < Ntext; ii++)
   {
      gentext(&text[ii].attr);                                                   //  make image from text and attributes
      text[ii].ww = text[ii].attr.pxb_text->ww;                                  //  text image size in layout
      text[ii].hh = text[ii].attr.pxb_text->hh;
      select("text",ii);
      setlayoutupdatearea();                                                     //  set layout update area
   }

   //  generate line/arrow images

   for (ii = 0; ii < Nline; ii++)                                                //  load overlay line/arrow images
   {
      genline(&line[ii].attr);                                                   //  make image from line attributes
      line[ii].ww = line[ii].attr.pxb_line->ww;                                  //  line image size in layout
      line[ii].hh = line[ii].attr.pxb_line->hh;
      select("line",ii);
      setlayoutupdatearea();                                                     //  set layout update area
   }

   viewmode('F');
   return 0;                                                                     //  success return

failure:
   if (fid) fclose(fid);
   if (nfid > -1) close(nfid);
   zmessageACK(Mwin,"%s \n %s \n %s \n %s",
                      masterfile,tranfile,warpfile,strerror(errno));
   goto cleanupErr;

badproject:                                                                      //  project file had a problem
   if (fid) fclose(fid);
   if (nfid > -1) close(nfid);
   zmessageACK(Mwin,"project file is defective");

cleanupErr:
   if (Lpxb) PXB_free(Lpxb);
   Lpxb = 0;
   for (ii = Nimage-1; ii >= 0; ii--)                                            //  free image memory
      remove_image(ii);
   for (ii = Ntext-1; ii >= 0; ii--)                                             //  free text memory
      remove_text(ii);
   for (ii = Nline-1; ii >= 0; ii--)                                             //  free line/arrow memory
      remove_line(ii);
   return 1;                                                                     //  failure return
}


//  save a mashup project so that it can be edited again later

void mashup::project_save()
{
   using namespace mashup;

   textattr_t  txattr;
   lineattr_t  lnattr;
   ch          text2[1200];
   ch          *pp, *ppx;
   ch          cpfile[200];
   FILE        *fid;
   STATB       statB;
   int         nfid, ii, kk, cc, vcc, wcc;

   pp = zdialog_text1(Mwin,"enter project name",projname);                       //  get project name from user 
   if (! pp) return;

   if (strchr(projname,'"') || strchr(projname,'\'')) { 
      zmessageACK(Mwin,"project name cannot have quote characters");
      return;
   }

   strncpy0(projname,pp,100);
   zfree(pp);

   snprintf(projfolder,200,"%s/%s",mashup_folder,projname);
   zshell(0,"mkdir -p %s",projfolder);
   if (! dirfile(projfolder,&statB)) return;

   snprintf(masterfile,200,"%s/masterdata",projfolder);                          //  project master data file
   snprintf(tranfile,200,"%s/trandata",projfolder);                              //  transparency data file
   snprintf(warpfile,200,"%s/warpdata",projfolder);                              //  warp data file

   if (PRscale > 1) project_rescale(1.0);                                        //  must save at 1x only

   fid = fopen(masterfile,"w");
   if (! fid) goto failure;

   if (*layoutfile) {
      ppx = strrchr(layoutfile,'.');                                             //  copy layout file 
      if (! ppx) goto failure;                                                   //    to /projfolder/layout.ext
      snprintf(cpfile,200,"%s/layoutfile.%s",projfolder,ppx+1);
      if (! strmatch(layoutfile,cpfile))
         zshell(0,"cp -T \"%s\" \"%s\" ",layoutfile,cpfile);
      fprintf(fid,"layout: %s\n",cpfile);
   }

   else fprintf(fid,"flatlayout: %d %d RGB: %d/%d/%d \n",                        //  write flat layout attributes
                     Lww,Lhh,flatRGB[0],flatRGB[1],flatRGB[2]);

   for (ii = 0; ii < Nimage; ii++)                                               //  write image data
   {
      ppx = strrchr(image[ii].file,'.');                                         //  copy image filename.ext 
      if (! ppx) continue;                                                       //    to /projfolder/NNN.ext
      snprintf(cpfile,200,"%s/%03d.%s",projfolder,ii,ppx+1);
      if (! strmatch(image[ii].file,cpfile))
         zshell(0,"cp -T \"%s\" \"%s\" ",image[ii].file,cpfile);

      fprintf(fid,"image: %s\n",cpfile);
      fprintf(fid,"position: %d %d \n", image[ii].px0, image[ii].py0);
      fprintf(fid,"fixed: %d \n",image[ii].fixed);
      fprintf(fid,"scale: %.4f \n",image[ii].scale);
      fprintf(fid,"theta: %.4f \n",image[ii].theta);
      fprintf(fid,"Btransp: %.4f \n",image[ii].Btransp);
      fprintf(fid,"Lmarg: %d \n",image[ii].Lmarg);
      fprintf(fid,"Rmarg: %d \n",image[ii].Rmarg);
      fprintf(fid,"Tmarg: %d \n",image[ii].Tmarg);
      fprintf(fid,"Bmarg: %d \n",image[ii].Bmarg);
      fprintf(fid,"Lblend: %d \n",image[ii].Lblend);
      fprintf(fid,"Rblend: %d \n",image[ii].Rblend);
      fprintf(fid,"Tblend: %d \n",image[ii].Tblend);
      fprintf(fid,"Bblend: %d \n",image[ii].Bblend);
      fprintf(fid,"Nwarp: %d \n",image[ii].Nwarp);
      for (kk = 0; kk < image[ii].Nwarp; kk++)
         fprintf(fid,"%f %f %f %f %f \n",
                  image[ii].warpmem[kk][0],image[ii].warpmem[kk][1],
                  image[ii].warpmem[kk][2],image[ii].warpmem[kk][3],image[ii].warpmem[kk][4]);
      fprintf(fid,"vtrancc: %d \n",image[ii].vtrancc);
      fprintf(fid,"warpcc: %d \n",image[ii].warpcc);
   }

   fprintf(fid,"end\n");

   for (ii = 0; ii < Ntext; ii++)                                                //  write text data
   {
      repl_Nstrs(text[ii].attr.text,text2,999,"\n","\\n",null);
      fprintf(fid,"text: %s\n",text2);
      fprintf(fid,"position: %d %d \n", text[ii].px0, text[ii].py0);
      txattr = text[ii].attr;
      fprintf(fid,"font: %s  size: %d \n", txattr.font, txattr.size);
      fprintf(fid,"attributes: %.4f  %s %s %s %s  %d %d %d %d  %d %d %d \n",
               txattr.angle, txattr.color[0], txattr.color[1], txattr.color[2], txattr.color[3],
               txattr.transp[0], txattr.transp[1], txattr.transp[2], txattr.transp[3],
               txattr.towidth, txattr.shwidth, txattr.shangle);
   }

   fprintf(fid,"end\n");

   for (ii = 0; ii < Nline; ii++)                                                //  write line/arrow data
   {
      fprintf(fid,"line:\n");
      fprintf(fid,"position: %d %d \n", line[ii].px0, line[ii].py0);
      lnattr = line[ii].attr;
      fprintf(fid,"attributes: %d %d %d %d %.4f  %s %s %s %s  %d %d %d %d  %d %d %d \n",
               lnattr.length, lnattr.width, lnattr.larrow, lnattr.rarrow, lnattr.angle,
               lnattr.color[0], lnattr.color[1], lnattr.color[2], lnattr.color[3],
               lnattr.transp[0], lnattr.transp[1], lnattr.transp[2], lnattr.transp[3],
               lnattr.towidth, lnattr.shwidth, lnattr.shangle);
   }

   fprintf(fid,"end\n");

   fclose(fid);

   for (vcc = ii = 0; ii < Nimage; ii++)                                         //  sum transparency data
      vcc += image[ii].vtrancc;

   if (vcc == 0) remove(tranfile);                                               //  no transparencies, remove poss. file
   else
   {
      nfid = open(tranfile,O_WRONLY|O_CREAT|O_TRUNC,0640);                       //  write transparencies to binary file
      if (nfid < 0) goto failure;

      for (ii = 0; ii < Nimage; ii++) {
         vcc = image[ii].vtrancc;
         if (! vcc) continue;
         cc = write(nfid,image[ii].vtranmap,vcc);
         if (cc != vcc) goto failure;
      }

      close(nfid);
   }

   for (wcc = ii = 0; ii < Nimage; ii++)                                         //  sum warpcc data
      wcc += image[ii].warpcc;

   if (wcc == 0) remove(warpfile);                                               //  no warp data, remove poss. file
   else
   {
      nfid = open(warpfile,O_WRONLY|O_CREAT|O_TRUNC,0640);                       //  write warp data to binary file
      if (nfid < 0) goto failure;

      for (ii = 0; ii < Nimage; ii++) {
         wcc = image[ii].warpcc;
         if (! wcc) continue;
         cc = write(nfid,image[ii].warpx,wcc);
         cc = write(nfid,image[ii].warpy,wcc);
         if (cc != wcc) goto failure;
      }

      close(nfid);
   }

   return;

failure:
   zmessageACK(Mwin,"cannot save layout file \n %s",strerror(errno));
   return;
}


//  scale project up by 2x/3x/4x or reset to 1x

void mashup::project_rescale(float scale)
{
   using namespace mashup;

   int      ii, nn;
   int64    size;
   float    scaleR;
   PXB      *pxbtemp;

   if (scale == 1.0) {
      if (PRscale == 2.0) scale = 0.5;
      if (PRscale == 3.0) scale = 0.3334;                                        //  int 3/6/9... --> 1/2/3... not 0/1/2...
      if (PRscale == 4.0) scale = 0.25;
   }

   size = Lww * Lhh * scale * scale;
   if (12 * size > (int64)(4000) * MEGA) {                                       //  PXM size < 4 GB to remain editable
      zmessageACK(Mwin,"layout exceeds 4 gigabytes");
      return;
   }

   if (scale < 1) PRscale = 1.0;                                                 //  reset to 1x
   else PRscale = scale;                                                         //  rescale to 2x, 3x or 4x

   Lww *= scale;                                                                 //  rescale layout image
   Lhh *= scale;
   pxbtemp = PXB_rescale(Lpxb,Lww,Lhh);
   PXB_free(Lpxb);
   Lpxb = pxbtemp;

   PXB_free(Fpxb);                                                               //  rescale window image to match
   Fpxb = PXB_copy(Lpxb);

   for (ii = 0; ii < Nimage; ii++) {                                             //  rescale overlay images
      scaleR = scale * image[ii].scale;
      image_rescale(ii,scaleR);
      image[ii].px0 *= scale;                                                    //  new layout position
      image[ii].py0 *= scale;
      select("image",ii);
      setlayoutupdatearea();
   }

   for (ii = 0; ii < Ntext; ii++) {                                              //  rescale text images
      text[ii].px0 *= scale;
      text[ii].py0 *= scale;
      text[ii].attr.size *= scale;
      text[ii].attr.towidth *= scale;
      text[ii].attr.shwidth *= scale;
      gentext(&text[ii].attr);
      text[ii].ww = text[ii].attr.pxb_text->ww;
      text[ii].hh = text[ii].attr.pxb_text->hh;
      select("text",ii);
      setlayoutupdatearea();
   }

   for (ii = 0; ii < Nline; ii++) {                                              //  rescale line images
      nn = line[ii].px0;
      nn = (nn+10) * scale - 10;                                                 //  compensate fixed 10 pixel margin
      line[ii].px0 = nn;
      nn = line[ii].py0;
      nn = (nn+10) * scale - 10;
      line[ii].py0 = nn;
      line[ii].attr.length *= scale;
      line[ii].attr.width *= scale;
      line[ii].attr.towidth *= scale;
      line[ii].attr.shwidth *= scale;
      genline(&line[ii].attr);
      line[ii].ww = line[ii].attr.pxb_line->ww;
      line[ii].hh = line[ii].attr.pxb_line->hh;
      select("line",ii);
      setlayoutupdatearea();
   }

   Fupdall = 1;
   Lupdate(2);
   return;
}


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

//  mashup image edit function

void mashup::image_edit()
{
   using namespace mashup;

   ch     *tipmess = "Click image to select, drag image to move.";
   ch     *blackmargmess = "Make black margins transparent";
   ch     *imagefixed = "Image is fixed, unmovable";

   if (! strmatch(focus,"image")) select("",-1);                                 //  nothing selected

/***
          _______________________________________________
         |              Edit Images                      |
         |                                               |
         |  Click image to select, drag image to move.   |
         |                                               |
         |  Current image: filename.jpg                  |
         |  Cycle through images: [Prev] [Next]          |
         |                                               |
         |  Scale [0.345 ] [ 1.0 ]  Angle [12.3 ]        |
         |  Stacking Order  [raise] [lower]              |
         |  Base Transparency [0.5 ]                     |
         |  Variable Transparency  [paint]               |
         |  Bend and fine-align  [warp]                  |
         |  [x] Make black margins transparent           |
         |  [x] Image is fixed, unmovable                |
         |                                               |
         |  Margins      hard       blend                |
         |   Left       [ 123 ]    [   0 ]               |
         |   Right      [   0 ]    [ 123 ]               |
         |   Top        [ 234 ]    [   0 ]               |
         |   Bottom     [   0 ]    [ 234 ]               |
         |                                               |
         |                      [Add Image] [Delete] [X] |
         |_______________________________________________|

***/

   zdimage = zdialog_new("Edit Images",Mwin,"Add Image","Delete"," X ",null);
   zdialog *zd = zdimage;

   zdialog_add_widget(zd,"hbox","hbtip","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labtip","hbtip",tipmess,"space=3");

   zdialog_add_widget(zd,"hbox","hbfile","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labfile","hbfile","Current image:","space=3");
   zdialog_add_widget(zd,"label","currfile","hbfile",0,"space=3");

   zdialog_add_widget(zd,"hbox","hbnext","dialog");
   zdialog_add_widget(zd,"label","labnext","hbnext","Cycle through images:","space=3");
   zdialog_add_widget(zd,"button","prev","hbnext","Prev","space=8");
   zdialog_add_widget(zd,"button","next","hbnext","Next");

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

   zdialog_add_widget(zd,"hbox","hbscale","dialog");
   zdialog_add_widget(zd,"label","labscale","hbscale","Scale","space=3");
   zdialog_add_widget(zd,"zspin","scale","hbscale","0.02|4.0|0.001|0.3");
   zdialog_add_widget(zd,"button","scale1x","hbscale"," 1.0 ","space=10");

   zdialog_add_widget(zd,"label","space","hbscale",0,"space=10"); 
   zdialog_add_widget(zd,"label","labangle","hbscale","Angle","space=3");
   zdialog_add_widget(zd,"zspin","angle","hbscale","-180|180|0.1|0");

   zdialog_add_widget(zd,"hbox","hbstack","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labstack","hbstack","Stacking Order","space=3");
   zdialog_add_widget(zd,"button","raise","hbstack","Raise","space=5");
   zdialog_add_widget(zd,"button","lower","hbstack","Lower","space=5");

   zdialog_add_widget(zd,"hbox","hbbtransp","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labbtr","hbbtransp","Base Transparency","space=3");
   zdialog_add_widget(zd,"zspin","Btransp","hbbtransp","0|1.0|0.01|0");

   zdialog_add_widget(zd,"hbox","hbvtran","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labvtran","hbvtran","Var. Transparency","space=3");
   zdialog_add_widget(zd,"button","vtran","hbvtran","Paint","space=5");

   zdialog_add_widget(zd,"hbox","hbwarp","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labwarp","hbwarp","Bend and fine-align","space=3");
   zdialog_add_widget(zd,"button","warp","hbwarp","Warp","space=5");

   zdialog_add_widget(zd,"hbox","hbmarg","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","fixmarg","hbmarg",blackmargmess,"space=3");

   zdialog_add_widget(zd,"hbox","hbfixed","dialog",0,"space=3");
   zdialog_add_widget(zd,"check","fixed","hbfixed",imagefixed,"space=3");

   zdialog_add_widget(zd,"hbox","hbmarg","dialog",0,"space=3");                  //  margins
   zdialog_add_widget(zd,"vbox","vbmarg1","hbmarg",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vbmarg2","hbmarg",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vbmarg3","hbmarg",0,"space=3|homog");
   zdialog_add_widget(zd,"label","labmarg","vbmarg1","Margins");
   zdialog_add_widget(zd,"label","labhard","vbmarg2","Hard");
   zdialog_add_widget(zd,"label","labblend","vbmarg3","Blend");
   zdialog_add_widget(zd,"label","lableft","vbmarg1","Left");
   zdialog_add_widget(zd,"label","labright","vbmarg1","Right");
   zdialog_add_widget(zd,"label","labtop","vbmarg1","Top");
   zdialog_add_widget(zd,"label","labbott","vbmarg1","Bottom");
   zdialog_add_widget(zd,"zspin","Lmarg","vbmarg2","0|999|1|0");
   zdialog_add_widget(zd,"zspin","Rmarg","vbmarg2","0|999|1|0");
   zdialog_add_widget(zd,"zspin","Tmarg","vbmarg2","0|999|1|0");
   zdialog_add_widget(zd,"zspin","Bmarg","vbmarg2","0|999|1|0");
   zdialog_add_widget(zd,"zspin","Lblend","vbmarg3","0|999|1|0");
   zdialog_add_widget(zd,"zspin","Rblend","vbmarg3","0|999|1|0");
   zdialog_add_widget(zd,"zspin","Tblend","vbmarg3","0|999|1|0");
   zdialog_add_widget(zd,"zspin","Bblend","vbmarg3","0|999|1|0");

   zdialog_add_ttip(zd,"Add Image","add images to layout");

   zdialog_run(zd,image_dialog_event,"save");                                    //  run dialog - parallel
   zdialog_wait(zd);
   zdialog_free(zd);
   return;
}


//  image dialog event and completion function

int mashup::image_dialog_event(zdialog *zd, ch *event)
{
   using namespace mashup;

   int      ii;
   float    scale, angle, rad = PI / 180.0;
   image_t  temp;

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  add image
         zd->zstat = 0;
         add_image();
         return 1;
      }

      if (zd->zstat == 2) {                                                      //  remove image
         zd->zstat = 0;
         if (! strmatch(focus,"image")) return 1;
         ii = focusii;
         remove_image(ii);
         return 1;
      }

      zdialog_free(zd);                                                          //  done or [x] kill

      zdimage = 0;

      if (zdtransp) {
         zdialog_free(zdtransp);
         zdtransp = 0;
      }

      if (zdwarp) {
         zdialog_free(zdwarp);
         zdwarp = 0;
      }

      return 1;
   }

   if (strmatch(event,"focus")) {                                                //  get mouse ownership
      takeMouse(mousefunc_layout,0);
      return 1;
   }

   if (strmatch(event,"next")) {                                                 //  select and flash next image
      if (! Nimage) return 1;
      if (strmatch(focus,"image")) {
         if (focusii == Nimage-1) select("image",0);
         else select("image",focusii+1);
      }
      else select("image",0);
      flashimage();
      Fnextimage = 1;                                                            //  flag for layout mouse func.
   }

   if (strmatch(event,"prev")) {                                                 //  select and flash prev. image
      if (! Nimage) return 1;
      if (strmatch(focus,"image")) {
         if (focusii == 0) select("image",Nimage-1);
         else select("image",focusii-1);
      }
      else select("image",0);
      flashimage();
      Fnextimage = 1;
   }

   if (! strmatch(focus,"image")) return 1;                                      //  get selected image
   ii = focusii;

   if (strmatch(event,"scale"))                                                  //  rescale
   {
      zdialog_fetch(zd,"scale",scale);                                           //  new image scale
      image_rescale(ii,scale);
      select("image",ii);
   }

   if (strmatch(event,"scale1x"))                                                //  rescale to 1x
   {
      zdialog_stuff(zd,"scale",1.0);
      image_rescale(ii,1.0);
      select("image",ii);
   }

   if (strmatch(event,"angle"))                                                  //  rotate
   {
      zdialog_fetch(zd,"angle",angle);
      angle = angle * rad;
      image[ii].theta = angle;
      image[ii].cosT = cos(angle);
      image[ii].sinT = sin(angle);
   }

   if (strmatch(event,"raise")) {                                                //  raise image in stacking order
      if (ii == Nimage - 1) return 1;
      temp = image[ii+1];
      image[ii+1] = image[ii];
      image[ii] = temp;
      ii = focusii = ii + 1;
   }

   if (strmatch(event,"lower")) {                                                //  lower image in stacking order
      if (ii == 0) return 1;
      temp = image[ii-1];
      image[ii-1] = image[ii];
      image[ii] = temp;
      ii = focusii = ii - 1;
   }

   if (strmatch(event,"Btransp"))                                                //  base transparency, 0 to 1.0 = invisible
      zdialog_fetch(zd,"Btransp",image[ii].Btransp);

   if (strmatch(event,"vtran")) {                                                //  paint variable transparency dialog
      paintransp_dialog();
      return 1;
   }

   if (strmatch(event,"warp")) {
      warp_dialog();
      return 1;
   }

   if (strmatch(event,"fixmarg")) transparent_margins();                         //  fix black margins

   if (strmatch(event,"fixed"))                                                  //  toggle fixed-image status
      zdialog_fetch(zd,"fixed",image[ii].fixed);

   if (strmatch(event,"Lmarg"))                                                  //  left edge margin
      zdialog_fetch(zd,"Lmarg",image[ii].Lmarg);

   if (strmatch(event,"Rmarg"))                                                  //  right edge
      zdialog_fetch(zd,"Rmarg",image[ii].Rmarg);

   if (strmatch(event,"Tmarg"))                                                  //  top edge
      zdialog_fetch(zd,"Tmarg",image[ii].Tmarg);

   if (strmatch(event,"Bmarg"))                                                  //  bottom edge
      zdialog_fetch(zd,"Bmarg",image[ii].Bmarg);

   if (strmatch(event,"Lblend"))                                                 //  left edge blend width
      zdialog_fetch(zd,"Lblend",image[ii].Lblend);

   if (strmatch(event,"Rblend"))                                                 //  right edge
      zdialog_fetch(zd,"Rblend",image[ii].Rblend);

   if (strmatch(event,"Tblend"))                                                 //  top edge
      zdialog_fetch(zd,"Tblend",image[ii].Tblend);

   if (strmatch(event,"Bblend"))                                                 //  bottom edge
      zdialog_fetch(zd,"Bblend",image[ii].Bblend);

   zmainloop();
   setlayoutupdatearea();                                                        //  set layout update area
   Lupdate(3);                                                                   //  update layout composite image

   return 1;
}


//  rescale an overlay image: pxb1 (1x) --> pxb2 (scale)
//  if warps are present, rescale from 1x and apply to pxb2

void mashup::image_rescale(int ii, float scale)
{
   int      ww1, hh1, ww2, hh2;
   int      px1, py1, px2, py2, kk;
   float    dx1, dy1, dx2, dy2, vstat;
   uint8    vpix[4], *pix2;
   PXB      *pxb2, *pxb2w;

   ww1 = image[ii].ww1;                                                          //  1x image size
   hh1 = image[ii].hh1;
   ww2 = scale * ww1;                                                            //  image size at scale
   hh2 = scale * hh1;
   pxb2 = PXB_rescale(image[ii].pxb1,ww2,hh2);                                   //  rescale image -> pxb2
   PXB_free(image[ii].pxb2);
   image[ii].pxb2 = pxb2;
   image[ii].ww2 = ww2;
   image[ii].hh2 = hh2;
   image[ii].scale = scale;

   if (image[ii].warpcc) {                                                       //  apply image warps to pxb2
      pxb2w = PXB_copy(pxb2);
      for (py2 = 0; py2 < hh2; py2++)
      for (px2 = 0; px2 < ww2; px2++) {
         px1 = px2 / scale;                                                      //  rescale 1x warps
         py1 = py2 / scale;
         kk = py1 * ww1 + px1;
         dx1 = image[ii].warpx[kk];
         dy1 = image[ii].warpy[kk];
         dx2 = dx1 * scale;
         dy2 = dy1 * scale;
         vstat = vpixel(image[ii].pxb2,px2+dx2,py2+dy2,vpix);
         pix2 = PXBpix(pxb2w,px2,py2);
         if (vstat) memcpy(pix2,vpix,3);
         else pix2[0] = pix2[1] = pix2[2] = pix2[3] = 0;
      }
      PXB_free(image[ii].pxb2);
      image[ii].pxb2 = pxb2w;
   }

   return;
}


//  dialog to initiate painting image variable transparency

void mashup::paintransp_dialog()
{
   using namespace mashup;

   ch     *ptitle = "Paint Image Transparencies";

/***
          _________________________________
         |  Paint Image Transparencies     |
         |                                 |
         | Radius [123 ]   [x] Gradual     |
         | Power  Center [100]  Edge [ 10] |
         |                                 |
         |                             [X] |
         |_________________________________|

***/

   zdtransp = zdialog_new(ptitle,Mwin," X ",null);
   zdialog_add_widget(zdtransp,"hbox","hbrad","dialog",0,"space=3");
   zdialog_add_widget(zdtransp,"label","labrad","hbrad","Radius","space=5");
   zdialog_add_widget(zdtransp,"zspin","radius","hbrad","5|300|1|100");
   zdialog_add_widget(zdtransp,"check","gradual","hbrad","Gradual","space=10");
   zdialog_add_widget(zdtransp,"hbox","hbpow","dialog",0,"space=3");
   zdialog_add_widget(zdtransp,"label","labpower","hbpow","Power","space=5");
   zdialog_add_widget(zdtransp,"label","labcen","hbpow","Center","space=5");
   zdialog_add_widget(zdtransp,"zspin","center","hbpow","0|100|1|10");
   zdialog_add_widget(zdtransp,"label","space","hbpow",0,"space=6");
   zdialog_add_widget(zdtransp,"label","labedge","hbpow","Edge","space=5");
   zdialog_add_widget(zdtransp,"zspin","edge","hbpow","0|100|1|3");

   zdialog_stuff(zdtransp,"radius",Mradius);
   zdialog_stuff(zdtransp,"radius",Mradius);
   zdialog_stuff(zdtransp,"gradual",Mgradual);
   zdialog_stuff(zdtransp,"edge",Mpowedge);

   zdialog_run(zdtransp,paintransp_dialog_event,"save");
   takeMouse(paintransp_mousefunc,0);
   return;
}


//  zdialog event and completion function

int mashup::paintransp_dialog_event(zdialog *zd, ch *event)
{
   using namespace mashup;

   if (zd->zstat) {                                                              //  done or cancel
      zdialog_free(zd);
      zdtransp = 0;
      takeMouse(mousefunc_layout,0);
      return 1;
   }

   if (strmatch(event,"focus")) {                                                //  take mouse ownership
      takeMouse(paintransp_mousefunc,0);
      return 1;
   }

   if (strmatch(event,"radius"))                                                 //  mouse radius value
      zdialog_fetch(zd,"radius",Mradius);

   if (strmatch(event,"gradual"))                                                //  gradual/instant paint option
      zdialog_fetch(zd,"gradual",Mgradual);

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

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

   return 1;
}


//  mouse function for painting image var. transparency

void mashup::paintransp_mousefunc()
{
   using namespace mashup;

   int         ii, Mii, jj, kk, vcc, mx, my;
   int         distx, disty, dist, mindist;
   int         pxlo, pylo, pxhi, pyhi;
   int         cenx, ceny, mrad = Mradius;
   int         ww1, hh1, ww2, hh2, transp, mtransp;
   int         dx, dy, qx, qy, rx, ry;
   float       sinT, cosT, scale;
   float       px0, py0, px1, py1, px2, py2;

   px1 = py1 = -1;                                                               //  no mouse position in image

   if (! strmatch(focus,"image")) {                                              //  if no image focus,
      draw_mousecircle(0,0,0,1,0);                                               //    no mouse circle
      gdk_window_set_cursor(gdkwin,0);
   }

   mx = Mxposn;                                                                  //  mouse position in layout
   my = Myposn;

   if (LMclick)                                                                  //  left click
   {
      LMclick = 0;                                                               //  stop main window click after return

      mx = Mxclick;                                                              //  click position
      my = Myclick;

      Mii = -1;                                                                  //  no selected image
      mindist = 9999;

      for (ii = 0; ii < Nimage; ii++)                                            //  find closest image center to mouse
      {
         px0 = image[ii].px0;                                                    //  image position and rotation
         py0 = image[ii].py0;                                                    //    in layout image space
         sinT = image[ii].sinT;
         cosT = image[ii].cosT;
         ww2 = image[ii].ww2;                                                    //  image size at scale
         hh2 = image[ii].hh2;
         px1 = (mx-px0) * cosT + (my-py0) * sinT;                                //  image pixel for layout position
         py1 = (my-py0) * cosT - (mx-px0) * sinT;
         if (px1 < 0 || px1 > ww2-1) continue;                                   //  not within image
         if (py1 < 0 || py1 > hh2-1) continue;
         pxlo = image[ii].pxlo;                                                  //  image extent
         pxhi = image[ii].pxhi;
         pylo = image[ii].pylo;
         pyhi = image[ii].pyhi;
         cenx = (pxlo + pxhi) / 2;                                               //  image center
         ceny = (pylo + pyhi) / 2;
         distx = mx - cenx;
         disty = my - ceny;
         dist = sqrtf(distx * distx + disty * disty);                            //  mouse/center distance
         if (dist < mindist) {
            mindist = dist;
            Mii = ii;                                                            //  save closest image center
         }
      }

      if (Mii < 0) {                                                             //  no image clicked
         select("",-1);                                                          //  no selected image
         draw_mousecircle(0,0,0,1,0);                                            //  no mouse circle
         gdk_window_set_cursor(gdkwin,0);
         return;
      }

      ii = Mii;
      select("image",ii);                                                        //  set selected image
      flashimage();                                                              //  flash selected image
      return;
   }

   if (! strmatch(focus,"image")) {                                              //  if no current image,
      draw_mousecircle(0,0,0,1,0);                                               //    no mouse circle
      gdk_window_set_cursor(gdkwin,0);
      return;
   }

   ii = focusii;                                                                 //  current image
   px0 = image[ii].px0;                                                          //  image position and rotation
   py0 = image[ii].py0;                                                          //    in layout image space
   sinT = image[ii].sinT;
   cosT = image[ii].cosT;
   ww1 = image[ii].ww1;                                                          //  image size at 1x
   hh1 = image[ii].hh1;
   ww2 = image[ii].ww2;                                                          //  image size at scale
   hh2 = image[ii].hh2;
   px2 = (mx-px0) * cosT + (my-py0) * sinT;                                      //  mouse position within scaled image
   py2 = (my-py0) * cosT - (mx-px0) * sinT;

   if (px2+mrad < 0 || px2-mrad > ww2 ||                                         //  entire circle off image
       py2+mrad < 0 || py2-mrad > hh2) {
      draw_mousecircle(0,0,0,1,0);                                               //  no mouse circle
      gdk_window_set_cursor(gdkwin,0);
      return;
   }

   draw_mousecircle(mx,my,mrad,0,0);                                             //  mouse within image, draw mouse circle
   gdk_window_set_cursor(gdkwin,dragcursor);
   zmainloop();                                                                  //  keep mouse circle visible

   if (! Mxdrag && ! Mydrag) return;                                             //  no drag underway

   Mxdrag = Mydrag = 0;                                                          //  stop main window drag after return

   if (! image[ii].vtranmap) {                                                   //  make transparency map if not already
      vcc = ww1 * hh1;                                                           //  transparency map is 1x scale
      image[ii].vtranmap = (uint8 *) zmalloc(vcc,"mashup");                      //    and one uint8 per pixel
      image[ii].vtrancc = vcc;
   }
   
   scale = image[ii].scale;

   qx = px2 / scale;                                                             //  mouse position in 1x image
   qy = py2 / scale;

   if (image[ii].warpcc) {                                                       //  image is warped                       24.60
      kk = qy * ww1 + qx;
      qx += image[ii].warpx[kk];                                                 //  source pixel in 1x image
      qy += image[ii].warpy[kk];
   }

   mrad = mrad / scale;                                                          //  mouse radius at 1x scale

   for (dy = -mrad; dy <= mrad; dy++)                                            //  loop pixels within mouse circle
   for (dx = -mrad; dx <= mrad; dx++)
   {
      dist = sqrtf(dx*dx + dy*dy);                                               //  distance from mouse center
      if (dist >= mrad) continue;                                                //  outside mouse circle

      if (Mgradual) {                                                            //  gradual paint transparency
         if (drandz() < 0.005) continue;                                         //  thin active points
         mtransp = Mpowcen * (mrad - dist) / mrad                                //  mouse power at distance from center
                 + Mpowedge * dist / mrad;                                       //    Mpowcen ... Mpowedge  scale 0-100
         mtransp = mtransp * 0.2 + 1;                                            //  slow down                             24.60
      }
      else mtransp = 255;                                                        //  instant paint transparency 

      rx = qx + dx;                                                              //  1x image pixel position
      ry = qy + dy;
      
      if (rx < 0 || rx > ww1-1) continue;                                        //  outside 1x image
      if (ry < 0 || ry > hh1-1) continue;

      jj = ry * ww1 + rx;                                                        //  get corresp. var. transparency
      transp = image[ii].vtranmap[jj];

      if (Mbutton == 1) {                                                        //  left mouse button
         transp += mtransp;                                                      //  increase image transparency
         if (transp > 255) transp = 255;                                         //    by mouse power
      }
      else {                                                                     //  right button
         transp -= mtransp;                                                      //  decrease
         if (transp < 0) transp = 0;
      }

      image[ii].vtranmap[jj] = transp;                                           //  adjusted image transparency
   }

   updxlo = mx - mrad;                                                           //  update area = mouse circle
   updxhi = mx + mrad;
   updylo = my - mrad;
   updyhi = my + mrad;

   setlayoutupdatearea();                                                        //  set layout update area
   Lupdate(4);                                                                   //  update layout composite image
   return;
}


//  dialog to initiate warping an image by dragging the mouse

namespace warp
{
   float    dragx, dragy, dragw, dragh;
   float    warpD, Wspan;
   int      Fdrag;
   PXB      *pxb2w;
   int      warpii;
};


void mashup::warp_dialog()
{
   using namespace mashup;
   using namespace warp;

   ch   *warp_message = "Pull on the image with the mouse.";

/***
          ___________________________________
         |           Warp Image              |
         |                                   |
         | Pull on the image with the mouse. |
         |                                   |
         |  [undo last]  [undo all]          |
         |  Warp span  [___]                 |
         |                                   |
         |                               [X] |
         |___________________________________|

***/

   zdwarp = zdialog_new("Warp Image",Mwin," X ",null);
   zdialog_add_widget(zdwarp,"label","lab1","dialog",warp_message,"space=3");
   zdialog_add_widget(zdwarp,"hbox","hb1","dialog",0,"space=8");
   zdialog_add_widget(zdwarp,"button","undolast","hb1","Undo Last","space=8");
   zdialog_add_widget(zdwarp,"button","undoall","hb1","Undo All","space=2");
   zdialog_add_widget(zdwarp,"hbox","hb2","dialog",0,"space=4");
   zdialog_add_widget(zdwarp,"label","lab2","hb2","warp span","space=8");
   zdialog_add_widget(zdwarp,"zspin","Wspan","hb2","0.00|1.0|0.01|0.5","space=1");

   Wspan = 0.5;
   Fdrag = 0;
   pxb2w = 0;
   warpii = -1;

   zdialog_run(zdwarp,warp_dialog_event,"save");                                 //  run dialog, parallel
   takeMouse(warp_mousefunc,0);                                                  //  connect mouse function
   return;
}


//  dialog event and completion callback function

int mashup::warp_dialog_event(zdialog * zd, ch *event)
{
   using namespace mashup;
   using namespace warp;

   int      ii, kk;

   if (zd->zstat)                                                                //  done or cancel
   {
      if (pxb2w) PXB_free(pxb2w);
      pxb2w = 0;
      zdialog_free(zd);                                                          //  kill dialog
      zdwarp = 0;
      takeMouse(mousefunc_layout,0);                                             //  restore mouse ownership
      return 1;
   }

   if (! strmatch(focus,"image")) return 1;                                      //  insure image focus
   ii = focusii;

   if (strmatch(event,"focus"))                                                  //  connect mouse function
      takeMouse(warp_mousefunc,dragcursor);

   if (strmatch(event,"undolast"))
   {
      if (image[ii].Nwarp == 1)
         event = "undoall";
      else if (image[ii].Nwarp) {
         kk = image[ii].Nwarp - 1;
         dragx = image[ii].warpmem[kk][0];                                       //  new warp = negative prior warp
         dragy = image[ii].warpmem[kk][1];
         dragw = -image[ii].warpmem[kk][2];
         dragh = -image[ii].warpmem[kk][3];
         Wspan = image[ii].warpmem[kk][4];
         zdialog_stuff(zd,"Wspan",Wspan);
         Fdrag = 0;
         warp_warpfunc();
         image[ii].Nwarp -= 1;
      }
   }

   if (strmatch(event,"undoall") && image[ii].warpcc) {                          //  undo all warps
      image[ii].Nwarp = 0;
      zfree(image[ii].warpx);
      zfree(image[ii].warpy);
      image[ii].warpx = image[ii].warpy = 0;
      image[ii].warpcc = 0;
   }

   if (strstr(event,"undo")) {
      image_rescale(ii,image[ii].scale);                                         //  pxb1 + warps -> pxb2
      if (pxb2w) PXB_free(pxb2w);
      pxb2w = PXB_copy(image[ii].pxb2);                                          //  base for next drag/warp
      setlayoutupdatearea();
      Lupdate(5);                                                                //  update layout image
   }

   if (strmatch(event,"Wspan"))
      zdialog_fetch(zd,"Wspan",Wspan);

   return 1;
}


//  warp mouse function
//  convert mouse drags to image warps

void mashup::warp_mousefunc()
{
   using namespace mashup;
   using namespace warp;

   int      ii, kk, wcc, Mii;
   int      mx, my, dx, dy, dw, dh;
   int      distx, disty, dist, mindist;
   int      pxlo, pylo, pxhi, pyhi;
   int      cenx, ceny, ww1, hh1, ww2, hh2;
   float    sinT, cosT, scale, px0, py0, px1, py1;

   if (LMclick)                                                                  //  left click
   {
      gdk_window_set_cursor(gdkwin,0);                                           //  no drag cursor

      LMclick = 0;                                                               //  stop main window click after return
      mx = Mxclick;                                                              //  click position
      my = Myclick;

      Mii = -1;                                                                  //  no selected image
      mindist = 9999;

      for (ii = 0; ii < Nimage; ii++)                                            //  find closest image center to mouse
      {
         px0 = image[ii].px0;                                                    //  image position and rotation
         py0 = image[ii].py0;                                                    //    in layout image space
         sinT = image[ii].sinT;
         cosT = image[ii].cosT;
         ww2 = image[ii].ww2;                                                    //  image size at scale
         hh2 = image[ii].hh2;
         px1 = (mx-px0) * cosT + (my-py0) * sinT;                                //  image pixel for layout position
         py1 = (my-py0) * cosT - (mx-px0) * sinT;
         if (px1 < 0 || px1 > ww2-1) continue;                                   //  not within image
         if (py1 < 0 || py1 > hh2-1) continue;
         pxlo = image[ii].pxlo;                                                  //  image extent
         pxhi = image[ii].pxhi;
         pylo = image[ii].pylo;
         pyhi = image[ii].pyhi;
         cenx = (pxlo + pxhi) / 2;                                               //  image center
         ceny = (pylo + pyhi) / 2;
         distx = mx - cenx;
         disty = my - ceny;
         dist = sqrtf(distx * distx + disty * disty);                            //  mouse/center distance
         if (dist < mindist) {
            mindist = dist;
            Mii = ii;                                                            //  save closest image center
         }
      }

      if (Mii < 0) {                                                             //  no image clicked
         select("",-1);                                                          //  no selected image
         return;
      }

      ii = Mii;
      select("image",ii);                                                        //  set selected image
      flashimage();                                                              //  flash selected image
      return;
   }

   if (! strmatch(focus,"image")) {                                              //  if no image focus,
      gdk_window_set_cursor(gdkwin,0);                                           //    no drag cursor
      return;
   }

   ii = focusii;                                                                 //  focus image

   mx = Mxposn;                                                                  //  mouse position in layout space
   my = Myposn;

   px0 = image[ii].px0;                                                          //  image position and rotation
   py0 = image[ii].py0;                                                          //    in layout space
   sinT = image[ii].sinT;
   cosT = image[ii].cosT;
   px1 = (mx-px0) * cosT + (my-py0) * sinT;                                      //  mouse position within scaled image
   py1 = (my-py0) * cosT - (mx-px0) * sinT;

   ww2 = image[ii].ww2;                                                          //  image size at scale
   hh2 = image[ii].hh2;

   if (px1 < 0 || px1 > ww2 || py1 < 0 || py1 > hh2) {                           //  mouse outside image
      gdk_window_set_cursor(gdkwin,0);                                           //  no drag cursor
      return;
   }

   gdk_window_set_cursor(gdkwin,dragcursor);                                     //  mouse inside image, drag cursor

   if (! pxb2w || (ii != warpii)) {                                              //  image changed
      if (pxb2w) PXB_free(pxb2w);                                                //  initial base image for warping
      pxb2w = PXB_copy(image[ii].pxb2);
      warpii = ii;
   }

   if (Mxdrag || Mydrag)                                                         //  mouse drag underway
   {
      if (! image[ii].warpcc)                                                    //  first warp for image
      {
         ww1 = image[ii].ww1;                                                    //  1x image
         hh1 = image[ii].hh1;
         wcc = ww1 * hh1 * sizeof(float);
         image[ii].warpx = (float *) zmalloc(wcc,"mashup");                      //  get memory for pixel displacements
         image[ii].warpy = (float *) zmalloc(wcc,"mashup");
         image[ii].warpcc = wcc;
         image[ii].Nwarp = 0;                                                    //  no warp memory data
      }

      dx = Mxdown;                                                               //  drag origin, image space
      dy = Mydown;
      dw = Mxdrag - Mxdown;                                                      //  drag increment
      dh = Mydrag - Mydown;
      Mxdrag = Mydrag = 0;                                                       //  reset

      px0 = image[ii].px0;
      py0 = image[ii].py0;
      sinT = image[ii].sinT;
      cosT = image[ii].cosT;

      dragx = (dx-px0) * cosT + (dy-py0) * sinT;                                 //  drag origin in scaled image
      dragy = (dy-py0) * cosT - (dx-px0) * sinT;

      dragw = dw * cosT + dh * sinT;                                             //  drag increment in scaled image
      dragh = dh * cosT - dw * sinT;

      warpD = ww2 + hh2;                                                         //  warp span
      warpD = warpD * Wspan;

      Fdrag = 1;                                                                 //  drag underway
      warp_warpfunc();                                                           //  drag pxb2 image
   }

   else if (Fdrag)                                                               //  drag complete
   {
      scale = image[ii].scale;                                                   //  overlay image scale

      dragx = dragx / scale;                                                     //  conv. drag vector to 1x scale
      dragy = dragy / scale;
      dragw = dragw / scale;
      dragh = dragh / scale;
      warpD = warpD / scale;

      Fdrag = 0;
      warp_warpfunc();                                                           //  accumulate pxb1 warp positions

      image_rescale(ii,scale);                                                   //  pxb1 + warps -> pxb2

      PXB_free(pxb2w);                                                           //  base for next drag/warp
      pxb2w = PXB_copy(image[ii].pxb2);

      if (image[ii].Nwarp == 200)                                                //  add drag to undo memory
      {
         image[ii].Nwarp = 199;                                                  //  full, remove oldest
         for (kk = 0; kk < image[ii].Nwarp; kk++)
         {
            image[ii].warpmem[kk][0] = image[ii].warpmem[kk+1][0];
            image[ii].warpmem[kk][1] = image[ii].warpmem[kk+1][1];
            image[ii].warpmem[kk][2] = image[ii].warpmem[kk+1][2];
            image[ii].warpmem[kk][3] = image[ii].warpmem[kk+1][3];
            image[ii].warpmem[kk][4] = image[ii].warpmem[kk+1][4];
         }
      }

      kk = image[ii].Nwarp;
      image[ii].warpmem[kk][0] = dragx;                                          //  save drag for undo
      image[ii].warpmem[kk][1] = dragy;
      image[ii].warpmem[kk][2] = dragw;
      image[ii].warpmem[kk][3] = dragh;
      image[ii].warpmem[kk][4] = Wspan;
      image[ii].Nwarp++;
   }

   setlayoutupdatearea();                                                        //  update layout image
   Lupdate(6);
   return;
}


//  warp image and accumulate warp memory
//  mouse at (dragx,dragy) is moved (dragw,dragh) pixels

void mashup::warp_warpfunc()
{
   using namespace mashup;
   using namespace warp;

   do_wthreads(warp_warpfunc_wthread,NSMP);                                      //  worker threads

   return;
}

void * mashup::warp_warpfunc_wthread(void *arg)                                  //  worker thread
{
   using namespace mashup;
   using namespace warp;

   int      index = *((int *) arg);
   int      ii, kk, px, py, vstat;
   int      ww1, hh1, ww2, hh2;
   float    d, mag, dx, dy;
   float    FD = 1.0 / (warpD * warpD);
   uint8    vpix[4], *pix2;

   ii = focusii;

   if (Fdrag)                                                                    //  drag underway
   {
      ww2 = image[ii].ww2;
      hh2 = image[ii].hh2;

      for (py = index; py < hh2; py += NSMP)                                     //  process all pxb2 pixels
      for (px = 0; px < ww2; px++)
      {
         d = (px-dragx)*(px-dragx) + (py-dragy)*(py-dragy);                      //  compute warp based on nearness
         mag = 1.0 - d * FD;                                                     //    to mouse position = drag center
         if (mag < 0) continue;

         mag = mag * mag;                                                        //  faster than pow(mag,4);
         mag = mag * mag;

         dx = -dragw * mag;                                                      //  displacement = drag * mag
         dy = -dragh * mag;

         vstat = vpixel(pxb2w,px+dx,py+dy,vpix);                                 //  drag/warp pxb2 image
         pix2 = PXBpix(image[ii].pxb2,px,py);
         if (vstat) memcpy(pix2,vpix,3);
         else pix2[0] = pix2[1] = pix2[2] = pix2[3] = 0;
      }
   }

   else                                                                          //  drag complete
   {
      ww1 = image[ii].ww1;
      hh1 = image[ii].hh1;

      for (py = index; py < hh1; py += NSMP)                                     //  process all pxb1 pixels
      for (px = 0; px < ww1; px++)
      {
         d = (px-dragx)*(px-dragx) + (py-dragy)*(py-dragy);                      //  compute warp based on nearness
         mag = 1.0 - d * FD;                                                     //    to mouse position = drag center
         if (mag < 0) continue;

         mag = mag * mag;                                                        //  faster than pow(mag,4);
         mag = mag * mag;

         dx = -dragw * mag;                                                      //  displacement = drag * mag
         dy = -dragh * mag;

         kk = py * ww1 + px;                                                     //  accumulate drag/warp
         image[ii].warpx[kk] += dx;                                              //    into pxb1 warp memory
         image[ii].warpy[kk] += dy;
      }
   }

   return 0;                                                                     //  exit thread
}


//  make black image margins transparent

void mashup::transparent_margins()
{
   int      ii, ww1, hh1, px, py;
   PXB      *pxb1;
   uint8    *pixel;

   for (ii = 0; ii < Nimage; ii++)                                               //  loop all images
   {
      ww1 = image[ii].ww1;                                                       //  image 1x size
      hh1 = image[ii].hh1;
      pxb1 = image[ii].pxb1;

      for (py = 0; py < hh1; py++)                                               //  loop all rows
      {
         for (px = 0; px < ww1/2; px++)                                          //  from left edge to middle
         {
            pixel = PXBpix(pxb1,px,py);
            if (pixel[0] > 0 || pixel[1] > 0 || pixel[2] > 1) break;             //  break out at first non-black pixel
            pixel[3] = 0;                                                        //  pixel has full transparency
         }

         for (px = ww1-1; px > ww1/2; px--)                                      //  from right edge to middle
         {
            pixel = PXBpix(pxb1,px,py);
            if (pixel[0] > 0 || pixel[1] > 0 || pixel[2] > 1) break;
            pixel[3] = 0;
         }
      }

      for (px = 0; px < ww1; px++)                                               //  from top to middle
      {
         for (py = 0; py < hh1/2; py++)
         {
            pixel = PXBpix(pxb1,px,py);
            if (pixel[0] > 0 || pixel[1] > 0 || pixel[2] > 1) break;
            pixel[3] = 0;
         }

         for (py = hh1-1; py > hh1/2; py--)                                      //  from bottom to middle
         {
            pixel = PXBpix(pxb1,px,py);
            if (pixel[0] > 0 || pixel[1] > 0 || pixel[2] > 1) break;
            pixel[3] = 0;
         }
      }

      image_rescale(ii,image[ii].scale);                                         //  update scaled image pxb2
   }

   Fupdall = 1;
   Lupdate(7);                                                                   //  update layout composite image
   return;
}


//  select and add one or more new overlay image files

void mashup::add_image()
{
   using namespace mashup;

   int         ii, jj, nn, cc, Nfiles = 0;
   int         ww1, hh1, ww2, hh2;
   float       scale;
   ch          **sfiles = 0, *file;
   PXB         *pxb1;

   zdialog_show(zdimage,0);                                                      //  hide parent dialog

   select_files(0,1);                                                            //  select image files from gallery       24.20

   Fpxb = PXB_copy(Lpxb);                                                        //  restore mashup view                   24.20
   viewmode('F');

   zdialog_show(zdimage,1);

   if (! SFcount) return;                                                        //  nothing selected

   sfiles = (ch **) zmalloc(SFcount * sizeof(ch *),"mashup");

   for (ii = 0; ii < SFcount; ii++)
      sfiles[ii] = SelFiles[ii];

   Nfiles = SFcount;
   SFcount = 0;

   for (nn = 0; nn < Nfiles; nn++)
   {
      if (Nimage == maxmash) {
         zmessageACK(Mwin,"exceed %d images",maxmash);
         break;
      }

      file = sfiles[nn];
      pxb1 = PXB_load(file,1);                                                   //  load image, ACK errors
      if (! pxb1) {
         zfree(file);
         continue;
      }

      PXB_addalpha(pxb1);                                                        //  add alpha channel if missing

      ii = Nimage++;                                                             //  new image
      cc = sizeof(image_t);                                                      //  clear image data
      memset(&image[ii],0,cc);

      image[ii].file = file;                                                     //  add image file to mashup list
      image[ii].pxb1 = pxb1;
      ww1 = pxb1->ww;
      hh1 = pxb1->hh;
      scale = 1.0;
      if (scale * ww1 / Lww > 0.3) scale = 0.3 * Lww / ww1;                      //  if image > 30% layout, reduce to 30%
      if (scale * hh1 / Lhh > 0.3) scale = 0.3 * Lhh / hh1;
      ww2 = ww1 * scale;
      hh2 = hh1 * scale;
      image[ii].scale = scale;
      image[ii].ww1 = ww1;
      image[ii].hh1 = hh1;
      image[ii].ww2 = ww2;
      image[ii].hh2 = hh2;
      image[ii].pxb2 = PXB_rescale(image[ii].pxb1,ww2,hh2);                      //  scaled image for display
      image[ii].theta = 0.0;
      image[ii].sinT = 0;
      image[ii].cosT = 1.0;
      jj = ii;
      while (jj >= 25) jj -= 25;
      image[ii].py0 = (jj / 5) * 0.20 * Lhh;                                     //  initial position
      image[ii].px0 = (jj % 5) * 0.20 * Lww;
      select("image",ii);
      setlayoutupdatearea();                                                     //  set layout update area
   }

   while (nn < Nfiles) zfree(sfiles[nn++]);                                      //  free unused files
   zfree(sfiles);

   select("",-1);                                                                //  nothing selected
   Fupdall = 1;
   Lupdate(8);                                                                   //  update layout composite image
   return;
}


//  remove an overlay image

void mashup::remove_image(int ii)
{
   using namespace mashup;

   select("image",ii);
   setlayoutupdatearea();                                                        //  set layout update area

   zfree(image[ii].file);                                                        //  free memory
   PXB_free(image[ii].pxb1);
   PXB_free(image[ii].pxb2);
   if (image[ii].vtranmap) zfree(image[ii].vtranmap);

   for (int jj = ii+1; jj < Nimage; jj++)                                        //  pack-down image list
      image[ii] = image[jj];
   Nimage--;                                                                     //  image count

   select("",-1);                                                                //  no selected image
   Fupdall = 1;
   Lupdate(9);                                                                   //  update layout composite image
   return;
}


//  flash a selected image as bright as possible for a short moment

void mashup::flashimage()
{
   int      ii, px, py, ww2, hh2;
   int      xlo, xhi, ylo, yhi;
   float    px0, py0, px1, py1, px2, py2;
   float    sinT, cosT;
   uint8    *pix2, vpix[4];
   PXB      *pxb2;

   if (! strmatch(focus,"image")) return;
   ii = focusii;

   xlo = image[ii].pxlo;                                                         //  overlay image extent in layout image
   xhi = image[ii].pxhi;
   ylo = image[ii].pylo;
   yhi = image[ii].pyhi;

   for (py = ylo; py < yhi; py++)
   for (px = xlo; px < xhi; px++)
   {
      px1 = px;
      py1 = py;
      px0 = image[ii].px0;                                                       //  overlay image position and rotation
      py0 = image[ii].py0;                                                       //    in layout image space
      pxb2 = image[ii].pxb2;
      sinT = image[ii].sinT;
      cosT = image[ii].cosT;
      ww2 = image[ii].ww2;                                                       //  image size at scale
      hh2 = image[ii].hh2;
      px2 = (px1-px0) * cosT + (py1-py0) * sinT;                                 //  overlay image pixel for layout position
      py2 = (py1-py0) * cosT - (px1-px0) * sinT;
      if (px2 < 0 || px2 > ww2-1) continue;                                      //  not within image
      if (py2 < 0 || py2 > hh2-1) continue;
      pix2 = PXBpix(Fpxb,px,py);                                                 //  output image pixel
      if (! vpixel(pxb2,px2,py2,vpix)) continue;                                 //  overlay image virt. pixel
      pix2[0] = vpix[1];                                                         //  rotate the colors
      pix2[1] = vpix[2];
      pix2[2] = vpix[0];
   }

   ww2 = xhi - xlo;
   hh2 = yhi - ylo;

   PXB_PXB_update(Fpxb,Mpxb,xlo,ylo,ww2,hh2);                                    //  Fpxb > Mpxb, scaled up or down
   Fpaint4(xlo,ylo,ww2,hh2,0);                                                   //  update drawing window from Mpxb
   zmainsleep(0.1);                                                              //  hold for a moment

   setlayoutupdatearea();                                                        //  set layout update area
   Lupdate(10);                                                                  //  update layout composite image
   return;
}


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

//  add text to image or revise existing text

void mashup::text_edit()
{
   using namespace mashup;

   ch     *intro = "Enter text, [Add] to layout, edit properties.";

/***
       __________________________________________________
      |              Edit Text                           |
      |                                                  |
      |   Enter text, [Add] to layout, edit properties.  |
      |                                                  |
      |  Text [______________________________________]   |
      |                                                  |
      |  [font] [FreeSans______________]  size [ 44 ]    |
      |                                                  |
      |            color   transp.   width    angle      |
      |  text     [_____]  [_____]           [_____]     |
      |  backing  [_____]  [_____]                       |
      |  outline  [_____]  [_____]  [_____]              |
      |  shadow   [_____]  [_____]  [_____]  [_____]     |
      |                                                  |
      |  Text File: [Open] [Save]                        |
      |                                                  |
      |                     [Clear] [Apply] [Delete] [X] |
      |__________________________________________________|

***/

   if (! strmatch(focus,"text")) select("",-1);                                  //  nothing selected

   zdtext = zdialog_new("Edit Text",Mwin,"Clear","Apply","Delete"," X ",null);
   zdialog *zd = zdtext;

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

   zdialog_add_widget(zd,"hbox","hbfont","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","fontbutt","hbfont","Font");
   zdialog_add_widget(zd,"zentry","fontname","hbfont","FreeSans","space=2|expand");
   zdialog_add_widget(zd,"label","labsize","hbfont","Size","space=2");
   zdialog_add_widget(zd,"zspin","fontsize","hbfont","8|500|1|40");

   zdialog_add_widget(zd,"hbox","hbcol","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vbcol1","hbcol",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbcol2","hbcol",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vbcol3","hbcol",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vbcol4","hbcol",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vbcol5","hbcol",0,"homog|space=2");

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

   zdialog_add_widget(zd,"label","labcol","vbcol2","Color");
   zdialog_add_widget(zd,"colorbutt","fgcolor","vbcol2","0|0|0");
   zdialog_add_widget(zd,"colorbutt","bgcolor","vbcol2","255|255|255");
   zdialog_add_widget(zd,"colorbutt","tocolor","vbcol2","255|0|0");
   zdialog_add_widget(zd,"colorbutt","shcolor","vbcol2","0|255|0");

   zdialog_add_widget(zd,"label","labcol","vbcol3","Transp.");
   zdialog_add_widget(zd,"zspin","fgtransp","vbcol3","0|100|1|0");
   zdialog_add_widget(zd,"zspin","bgtransp","vbcol3","0|100|1|0");
   zdialog_add_widget(zd,"zspin","totransp","vbcol3","0|100|1|0");
   zdialog_add_widget(zd,"zspin","shtransp","vbcol3","0|100|1|0");

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

   zdialog_add_widget(zd,"label","labw","vbcol5","Angle");
   zdialog_add_widget(zd,"zspin","angle","vbcol5","-180|180|0.1|0");
   zdialog_add_widget(zd,"label","space","vbcol5");
   zdialog_add_widget(zd,"label","space","vbcol5");
   zdialog_add_widget(zd,"zspin","shangle","vbcol5","-180|180|1|0");

   zdialog_add_widget(zd,"hbox","hbaf","dialog",0,"space=10");
   zdialog_add_widget(zd,"label","labbg","hbaf","Text File:","space=3");
   zdialog_add_widget(zd,"button","loadtext","hbaf","Open","space=5");
   zdialog_add_widget(zd,"button","savetext","hbaf","Save","space=5");

   zdialog_add_ttip(zd,"Clear","clear entered text");
   zdialog_add_ttip(zd,"Apply","add entered text to layout");
   zdialog_add_ttip(zd,"Delete","delete selected text from layout");
   zdialog_add_ttip(zd,"OK","commit all changes, close dialog");

   zdialog_load_inputs(zd);                                                      //  restore prior inputs
   zdialog_run(zd,text_dialog_event,"save");                                     //  run dialog, parallel
   zdialog_wait(zd);
   zdialog_free(zd);

   return;
}


//  dialog event and completion callback function

int mashup::text_dialog_event(zdialog *zd, ch *event)
{
   using namespace mashup;

   GtkWidget   *font_dialog;
   ch          font[80], fname[70];
   int         ii, size;
   ch          *pp;

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  [clear]
         zd->zstat = 0;
         zdialog_stuff(zd,"text","");
         focus = "";
         return 1;
      }

      if (zd->zstat == 2) {                                                      //  [apply]
         zd->zstat = 0;
         add_text();                                                             //  zdialog inputs >> new text image
         return 1;
      }

      if (zd->zstat == 3) {                                                      //  [delete]
         zd->zstat = 0;
         if (! strmatch(focus,"text")) return 1;
         ii = focusii;
         remove_text(ii);                                                        //  remove selected text image
         return 1;
      }

      zdialog_free(zd);                                                          //  [OK] or [x] kill
      zdtext = 0;
      return 1;
   }

   if (strmatch(event,"focus")) {                                                //  get mouse ownership
      takeMouse(mousefunc_layout,0);
      return 1;
   }

   if (strmatch(event,"loadtext"))                                               //  load text data from file
      load_text(zdtext);                                                         //    and stuff zdialog fields

   if (strmatch(event,"savetext")) {                                             //  get all zdialog fields
      save_text(zdtext);                                                         //    and save them to a file
      return 1;
   }

   if (strmatch(event,"fontbutt"))                                               //  set new font
   {
      zdialog_fetch(zd,"fontname",fname,70);
      zdialog_fetch(zd,"fontsize",size);
      snprintf(font,80,"%s %d",fname,size);
      font_dialog = gtk_font_chooser_dialog_new("select font",MWIN);
      gtk_font_chooser_set_font(GTK_FONT_CHOOSER(font_dialog),font);
      gtk_dialog_run(GTK_DIALOG(font_dialog));
      pp = gtk_font_chooser_get_font(GTK_FONT_CHOOSER(font_dialog));
      gtk_widget_destroy(font_dialog);

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

   if (! strmatch(focus,"text")) return 1;                                       //  selected text image
   ii = focusii;

   zdialog_fetch(zd,"text",text[ii].attr.text,1000);                             //  initz. new text and attributes
   zdialog_fetch(zd,"fontname",text[ii].attr.font,80);
   zdialog_fetch(zd,"fontsize",text[ii].attr.size);
   zdialog_fetch(zd,"angle",text[ii].attr.angle);
   zdialog_fetch(zd,"fgcolor",text[ii].attr.color[0],20);
   zdialog_fetch(zd,"bgcolor",text[ii].attr.color[1],20);
   zdialog_fetch(zd,"tocolor",text[ii].attr.color[2],20);
   zdialog_fetch(zd,"shcolor",text[ii].attr.color[3],20);
   zdialog_fetch(zd,"fgtransp",text[ii].attr.transp[0]);
   zdialog_fetch(zd,"bgtransp",text[ii].attr.transp[1]);
   zdialog_fetch(zd,"totransp",text[ii].attr.transp[2]);
   zdialog_fetch(zd,"shtransp",text[ii].attr.transp[3]);
   zdialog_fetch(zd,"towidth",text[ii].attr.towidth);
   zdialog_fetch(zd,"shwidth",text[ii].attr.shwidth);
   zdialog_fetch(zd,"shangle",text[ii].attr.shangle);

   gentext(&text[ii].attr);                                                      //  generate new text image
   text[ii].ww = text[ii].attr.pxb_text->ww;
   text[ii].hh = text[ii].attr.pxb_text->hh;
   zmainloop();                                                                  //  update dialog controls

   setlayoutupdatearea();                                                        //  set layout update area
   Lupdate(11);                                                                  //  update layout composite image

   return 1;
}


//  add new text entry to text[ii] list
//  get text and attributes from zdialog fields

void mashup::add_text()
{
   using namespace mashup;

   int         ii;
   zdialog     *zd = zdtext;
   ch          *tip = "click position to add text";

   if (! zd) return;
   if (zdaddtext) return;

   if (Ntext == maxmash) {
      zmessageACK(Mwin,"exceeded %d text entries",maxmash);
      return;
   }

   Mxclick = Myclick = 0;
   zdaddtext = zdialog_new("Add Text",Mwin," X ",null);                          //  get mouse click for text position
   zdialog_set_decorated(zdaddtext,0);
   zdialog_add_widget(zdaddtext,"label","labtip","dialog",tip,"space=3");
   zdialog_run(zdaddtext,0,"mouse");
   zdialog_wait(zdaddtext);
   zdialog_free(zdaddtext);
   if (! (Mxclick + Myclick)) return;

   ii = Ntext++;
   memset(&text[ii],0,sizeof(text_t));

   zdialog_fetch(zd,"text",text[ii].attr.text,1000);                             //  initz. new text and attributes
   zdialog_fetch(zd,"fontname",text[ii].attr.font,80);
   zdialog_fetch(zd,"fontsize",text[ii].attr.size);
   zdialog_fetch(zd,"angle",text[ii].attr.angle);
   zdialog_fetch(zd,"fgcolor",text[ii].attr.color[0],20);
   zdialog_fetch(zd,"bgcolor",text[ii].attr.color[1],20);
   zdialog_fetch(zd,"tocolor",text[ii].attr.color[2],20);
   zdialog_fetch(zd,"shcolor",text[ii].attr.color[3],20);
   zdialog_fetch(zd,"fgtransp",text[ii].attr.transp[0]);
   zdialog_fetch(zd,"bgtransp",text[ii].attr.transp[1]);
   zdialog_fetch(zd,"totransp",text[ii].attr.transp[2]);
   zdialog_fetch(zd,"shtransp",text[ii].attr.transp[3]);
   zdialog_fetch(zd,"towidth",text[ii].attr.towidth);
   zdialog_fetch(zd,"shwidth",text[ii].attr.shwidth);
   zdialog_fetch(zd,"shangle",text[ii].attr.shangle);

   gentext(&text[ii].attr);                                                      //  generate new text image
   text[ii].ww = text[ii].attr.pxb_text->ww;
   text[ii].hh = text[ii].attr.pxb_text->hh;
   text[ii].px0 = Mxclick;                                                       //  set initial position
   text[ii].py0 = Myclick;

   select("text",ii);                                                            //  set selected text image
   setlayoutupdatearea();                                                        //  set layout update area
   Lupdate(12);                                                                  //  update layout composite image

   return;
}


//  remove text entry from text[ii]

void mashup::remove_text(int ii)
{
   using namespace mashup;

   select("text",ii);
   setlayoutupdatearea();                                                        //  set layout update area

   PXB_free(text[ii].attr.pxb_text);
   for (int jj = ii+1; jj < Ntext; jj++)                                         //  pack down text list
      text[jj-1] = text[jj];
   Ntext--;                                                                      //  text count

   select("",-1);                                                                //  nothing selected
   Fupdall = 1;
   Lupdate(13);                                                                  //  update layout composite image
   return;
}


//  flash a selected text image as bright as possible for a short moment

void mashup::flashtext()
{
   int         ii, jj, red, green, blue;
   ch          savecolor[4][20], flashcolor[20];

   if (! strmatch(focus,"text")) return;
   ii = focusii;

   for (jj = 0; jj < 4; jj++)                                                    //  save text colors
      strcpy(savecolor[jj],text[ii].attr.color[jj]);

   for (jj = 0; jj < 4; jj++) {                                                  //  get complimentary colors
      red = 255 - atoi(substring(text[ii].attr.color[jj],'|',1));
      green = 255 - atoi(substring(text[ii].attr.color[jj],'|',2));
      blue = 255 - atoi(substring(text[ii].attr.color[jj],'|',3));
      snprintf(flashcolor,20,"%d|%d|%d",red,green,blue);
      strcpy(text[ii].attr.color[jj],flashcolor);
   }

   gentext(&text[ii].attr);                                                      //  generate complimentary text image
   zmainloop();

   setlayoutupdatearea();                                                        //  set layout update area
   Lupdate(14);                                                                  //  update layout composite image
   zsleep(0.05);                                                                 //  hold for a moment

   for (jj = 0; jj < 4; jj++)                                                    //  restore text colors
      strcpy(text[ii].attr.color[jj],savecolor[jj]);
   gentext(&text[ii].attr);                                                      //  gemerate normal text image
   zmainloop();

   setlayoutupdatearea();                                                        //  set layout update area
   Lupdate(15);                                                                  //  update layout composite image
   return;
}


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

//  add a line/arrow to image or revise existing line/arrow

void mashup::line_edit()
{
   using namespace mashup;

   ch     *intro = "Set line properties, [Add] to layout, edit.";

/***
       _______________________________________________
      |            Edit Line/Arrow                    |
      |                                               |
      |  Set line properties, [Add] to layout, edit.  |
      |                                               |
      |  Line length [____]  width [____]             |
      |  Arrow head  [x] left   [x] right             |
      |                                               |
      |            color   transp.   width    angle   |
      |  line     [_____]  [_____]           [_____]  |
      |  backing  [_____]  [_____]                    |
      |  outline  [_____]  [_____]  [_____]           |
      |  shadow   [_____]  [_____]  [_____]  [_____]  |
      |                                               |
      |                            [Add] [Delete] [X] |
      |_______________________________________________|

***/

   if (! strmatch(focus,"line")) select("",-1);                                  //  nothing selected

   zdline = zdialog_new("Edit Line/Arrow",Mwin,"Add","Delete"," X ",null);
   zdialog *zd = zdline;

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

   zdialog_add_widget(zd,"hbox","hbline","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","lablength","hbline","Line length","space=5");
   zdialog_add_widget(zd,"zspin","length","hbline","2|9999|1|20");
   zdialog_add_widget(zd,"label","space","hbline",0,"space=10");
   zdialog_add_widget(zd,"label","labwidth","hbline","Width","space=5");
   zdialog_add_widget(zd,"zspin","width","hbline","1|99|1|2");

   zdialog_add_widget(zd,"hbox","hbarrow","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labarrow","hbarrow","Arrow head","space=5");
   zdialog_add_widget(zd,"check","larrow","hbarrow","Left");
   zdialog_add_widget(zd,"label","space","hbarrow",0,"space=10");
   zdialog_add_widget(zd,"check","rarrow","hbarrow","Right");

   zdialog_add_widget(zd,"hbox","hbcol","dialog");
   zdialog_add_widget(zd,"vbox","vbcol1","hbcol",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vbcol2","hbcol",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vbcol3","hbcol",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vbcol4","hbcol",0,"homog|space=2");
   zdialog_add_widget(zd,"vbox","vbcol5","hbcol",0,"homog|space=2");

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

   zdialog_add_widget(zd,"label","labcol","vbcol2","Color");
   zdialog_add_widget(zd,"colorbutt","fgcolor","vbcol2","0|0|0");
   zdialog_add_widget(zd,"colorbutt","bgcolor","vbcol2","255|255|255");
   zdialog_add_widget(zd,"colorbutt","tocolor","vbcol2","255|0|0");
   zdialog_add_widget(zd,"colorbutt","shcolor","vbcol2","0|255|0");

   zdialog_add_widget(zd,"label","labcol","vbcol3","Transp.");
   zdialog_add_widget(zd,"zspin","fgtransp","vbcol3","0|100|1|0");
   zdialog_add_widget(zd,"zspin","bgtransp","vbcol3","0|100|1|0");
   zdialog_add_widget(zd,"zspin","totransp","vbcol3","0|100|1|0");
   zdialog_add_widget(zd,"zspin","shtransp","vbcol3","0|100|1|0");

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

   zdialog_add_widget(zd,"label","labw","vbcol5","Angle");
   zdialog_add_widget(zd,"zspin","angle","vbcol5","-180|180|0.1|0");
   zdialog_add_widget(zd,"label","space","vbcol5");
   zdialog_add_widget(zd,"label","space","vbcol5");
   zdialog_add_widget(zd,"zspin","shangle","vbcol5","-180|180|1|0");

   zdialog_add_ttip(zd,"Add","add line/arrow to layout");

   zdialog_load_inputs(zd);                                                      //  restore prior inputs
   zdialog_run(zd,line_dialog_event,"save");                                     //  run dialog, parallel
   zdialog_wait(zd);
   zdialog_free(zd);

   return;
}


//  dialog event and completion callback function

int mashup::line_dialog_event(zdialog *zd, ch *event)
{
   using namespace mashup;

   int      ii;

   if (zd->zstat)
   {
      if (zd->zstat == 1) {                                                      //  add
         zd->zstat = 0;
         add_line();                                                             //  zdialog inputs >> new line/arrow image
         return 1;
      }

      if (zd->zstat == 2) {                                                      //  delete
         zd->zstat = 0;
         if (! strmatch(focus,"line")) return 1;
         ii = focusii;
         remove_line(ii);                                                        //  remove selected line/arrow image
         return 1;
      }

      zdialog_free(zd);                                                          //  done or [x] kill
      zdline = 0;
      return 1;
   }

   if (strmatch(event,"focus")) {                                                //  get mouse ownership
      takeMouse(mousefunc_layout,0);
      return 1;
   }

   ii = focusii;
   if (ii < 0) return 1;

   zdialog_fetch(zd,"length",line[ii].attr.length);
   zdialog_fetch(zd,"width",line[ii].attr.width);
   zdialog_fetch(zd,"larrow",line[ii].attr.larrow);
   zdialog_fetch(zd,"rarrow",line[ii].attr.rarrow);
   zdialog_fetch(zd,"angle",line[ii].attr.angle);
   zdialog_fetch(zd,"fgcolor",line[ii].attr.color[0],20);
   zdialog_fetch(zd,"bgcolor",line[ii].attr.color[1],20);
   zdialog_fetch(zd,"tocolor",line[ii].attr.color[2],20);
   zdialog_fetch(zd,"shcolor",line[ii].attr.color[3],20);
   zdialog_fetch(zd,"fgtransp",line[ii].attr.transp[0]);
   zdialog_fetch(zd,"bgtransp",line[ii].attr.transp[1]);
   zdialog_fetch(zd,"totransp",line[ii].attr.transp[2]);
   zdialog_fetch(zd,"shtransp",line[ii].attr.transp[3]);
   zdialog_fetch(zd,"towidth",line[ii].attr.towidth);
   zdialog_fetch(zd,"shwidth",line[ii].attr.shwidth);
   zdialog_fetch(zd,"shangle",line[ii].attr.shangle);

   genline(&line[ii].attr);                                                      //  generate new line/arrow image
   zmainloop();

   line[ii].ww = line[ii].attr.pxb_line->ww;
   line[ii].hh = line[ii].attr.pxb_line->hh;

   setlayoutupdatearea();                                                        //  set layout update area
   Lupdate(16);                                                                  //  update layout composite image

   return 1;
}


//  add new line/arrow entry to line[ii] list
//  get line attributes from zdialog fields

void mashup::add_line()
{
   using namespace mashup;

   int         ii;
   zdialog     *zd = zdline;
   ch          *tip = "click position to add line";

   if (! zd) return;
   if (zdaddline) return;

   if (Nline == maxmash) {
      zmessageACK(Mwin,"exceeded %d line entries",maxmash);
      return;
   }

   Mxclick = Myclick = 0;
   zdaddline = zdialog_new("Add Line",Mwin," X ",null);                          //  get mouse click for line position
   zdialog_set_decorated(zdaddline,0);
   zdialog_add_widget(zdaddline,"label","labtip","dialog",tip,"space=3");
   zdialog_run(zdaddline,0,"mouse");
   zdialog_wait(zdaddline);
   zdialog_free(zdaddline);
   if (! (Mxclick + Myclick)) return;

   ii = Nline++;
   memset(&line[ii],0,sizeof(line_t));

   zdialog_fetch(zd,"length",line[ii].attr.length);
   zdialog_fetch(zd,"width",line[ii].attr.width);
   zdialog_fetch(zd,"larrow",line[ii].attr.larrow);
   zdialog_fetch(zd,"rarrow",line[ii].attr.rarrow);
   zdialog_fetch(zd,"angle",line[ii].attr.angle);
   zdialog_fetch(zd,"fgcolor",line[ii].attr.color[0],20);
   zdialog_fetch(zd,"bgcolor",line[ii].attr.color[1],20);
   zdialog_fetch(zd,"tocolor",line[ii].attr.color[2],20);
   zdialog_fetch(zd,"shcolor",line[ii].attr.color[3],20);
   zdialog_fetch(zd,"fgtransp",line[ii].attr.transp[0]);
   zdialog_fetch(zd,"bgtransp",line[ii].attr.transp[1]);
   zdialog_fetch(zd,"totransp",line[ii].attr.transp[2]);
   zdialog_fetch(zd,"shtransp",line[ii].attr.transp[3]);
   zdialog_fetch(zd,"towidth",line[ii].attr.towidth);
   zdialog_fetch(zd,"shwidth",line[ii].attr.shwidth);
   zdialog_fetch(zd,"shangle",line[ii].attr.shangle);

   genline(&line[ii].attr);                                                      //  generate new line/arrow image
   line[ii].ww = line[ii].attr.pxb_line->ww;
   line[ii].hh = line[ii].attr.pxb_line->hh;
   line[ii].px0 = Mxclick;                                                       //  initial position
   line[ii].py0 = Myclick;

   select("line",ii);                                                            //  set selected line/arrow image
   setlayoutupdatearea();                                                        //  set layout update area
   Lupdate(17);                                                                  //  update layout composite image
   return;
}


//  remove line/arrow entry from line[ii]

void mashup::remove_line(int ii)
{
   using namespace mashup;

   select("line",ii);
   setlayoutupdatearea();                                                        //  set layout update area

   PXB_free(line[ii].attr.pxb_line);
   for (int jj = ii+1; jj < Nline; jj++)                                         //  pack down line list
      line[jj-1] = line[jj];
   Nline--;                                                                      //  line/arrow count

   select("",-1);                                                                //  nothing selected
   Fupdall = 1;
   Lupdate(18);                                                                  //  update layout composite image
   return;
}


//  flash a selected line/arrow image as bright as possible for a short moment

void mashup::flashline()
{
   int         ii, jj, red, green, blue;
   ch          savecolor[4][20], flashcolor[20];

   if (! strmatch(focus,"line")) return;
   ii = focusii;

   for (jj = 0; jj < 4; jj++)                                                    //  save line colors
      strcpy(savecolor[jj],line[ii].attr.color[jj]);

   for (jj = 0; jj < 4; jj++) {                                                  //  get complimentary colors
      red = 255 - atoi(substring(line[ii].attr.color[jj],'|',1));
      green = 255 - atoi(substring(line[ii].attr.color[jj],'|',2));
      blue = 255 - atoi(substring(line[ii].attr.color[jj],'|',3));
      snprintf(flashcolor,20,"%d|%d|%d",red,green,blue);
      strcpy(line[ii].attr.color[jj],flashcolor);
   }

   genline(&line[ii].attr);                                                      //  generate complimentary image
   zmainloop();

   setlayoutupdatearea();                                                        //  set layout update area
   Lupdate(19);                                                                  //  update layout composite image
   zsleep(0.5);                                                                  //  hold for a moment

   for (jj = 0; jj < 4; jj++)                                                    //  restore line colors
      strcpy(line[ii].attr.color[jj],savecolor[jj]);
   genline(&line[ii].attr);                                                      //  gemerate normal line image
   zmainloop();

   setlayoutupdatearea();                                                        //  set layout update area
   Lupdate(20);                                                                  //  update layout composite image
   return;
}


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

//  select an image, text image, or line/arrow image - update active dialog
//  which = "image" or "text" or "line" or "" to select nothing

void mashup::select(ch *which, int ii)
{
   using namespace mashup;

   ch       *pp;
   float    rad = 180 / PI;
   zdialog  *zd;

   if (strmatch(which,"image") && ii >= 0)
   {
      if (ii >= Nimage) zappcrash("bad image index");
      focus = "image";                                                           //  for KB key drags
      focusii = ii;

      zd = zdimage;
      if (! zd) return;                                                          //  no active edit image dialog

      pp = strrchr(image[ii].file,'/');                                          //  stuff zdialog fields
      zdialog_stuff(zd,"currfile",pp+1);                                         //    from selected image
      zdialog_stuff(zd,"scale",image[ii].scale);
      zdialog_stuff(zd,"angle",image[ii].theta * rad);
      zdialog_stuff(zd,"Btransp",image[ii].Btransp);
      zdialog_stuff(zd,"fixed",image[ii].fixed);
      zdialog_stuff(zd,"Lmarg",image[ii].Lmarg);
      zdialog_stuff(zd,"Rmarg",image[ii].Rmarg);
      zdialog_stuff(zd,"Tmarg",image[ii].Tmarg);
      zdialog_stuff(zd,"Bmarg",image[ii].Bmarg);
      zdialog_stuff(zd,"Lblend",image[ii].Lblend);
      zdialog_stuff(zd,"Rblend",image[ii].Rblend);
      zdialog_stuff(zd,"Tblend",image[ii].Tblend);
      zdialog_stuff(zd,"Bblend",image[ii].Bblend);
      return;
   }

   if (strmatch(which,"text") && ii >= 0)
   {
      if (ii >= Ntext) zappcrash("bad text index");
      focus = "text";                                                            //  for KB key drags
      focusii = ii;

      zd = zdtext;
      if (! zd) return;                                                          //  no active edit text dialog

      zdialog_stuff(zd,"text",text[ii].attr.text);                               //  stuff zdialog fields
      zdialog_stuff(zd,"fontname",text[ii].attr.font);                           //    from selected text image
      zdialog_stuff(zd,"fontsize",text[ii].attr.size);
      zdialog_stuff(zd,"angle",text[ii].attr.angle);
      zdialog_stuff(zd,"fgcolor",text[ii].attr.color[0]);
      zdialog_stuff(zd,"bgcolor",text[ii].attr.color[1]);
      zdialog_stuff(zd,"tocolor",text[ii].attr.color[2]);
      zdialog_stuff(zd,"shcolor",text[ii].attr.color[3]);
      zdialog_stuff(zd,"fgtransp",text[ii].attr.transp[0]);
      zdialog_stuff(zd,"bgtransp",text[ii].attr.transp[1]);
      zdialog_stuff(zd,"totransp",text[ii].attr.transp[2]);
      zdialog_stuff(zd,"shtransp",text[ii].attr.transp[3]);
      zdialog_stuff(zd,"towidth",text[ii].attr.towidth);
      zdialog_stuff(zd,"shwidth",text[ii].attr.shwidth);
      zdialog_stuff(zd,"shangle",text[ii].attr.shangle);
      return;
   }

   if (strmatch(which,"line") && ii >= 0)
   {
      if (ii >= Nline) zappcrash("bad line index");
      focus = "line";                                                            //  for KB key drags
      focusii = ii;

      zd = zdline;
      if (! zd) return;                                                          //  no active edit line/arrow dialog

      zdialog_stuff(zd,"length",line[ii].attr.length);
      zdialog_stuff(zd,"width",line[ii].attr.width);
      zdialog_stuff(zd,"larrow",line[ii].attr.larrow);
      zdialog_stuff(zd,"rarrow",line[ii].attr.rarrow);
      zdialog_stuff(zd,"angle",line[ii].attr.angle);
      zdialog_stuff(zd,"fgcolor",line[ii].attr.color[0]);
      zdialog_stuff(zd,"bgcolor",line[ii].attr.color[1]);
      zdialog_stuff(zd,"tocolor",line[ii].attr.color[2]);
      zdialog_stuff(zd,"shcolor",line[ii].attr.color[3]);
      zdialog_stuff(zd,"fgtransp",line[ii].attr.transp[0]);
      zdialog_stuff(zd,"bgtransp",line[ii].attr.transp[1]);
      zdialog_stuff(zd,"totransp",line[ii].attr.transp[2]);
      zdialog_stuff(zd,"shtransp",line[ii].attr.transp[3]);
      zdialog_stuff(zd,"towidth",line[ii].attr.towidth);
      zdialog_stuff(zd,"shwidth",line[ii].attr.shwidth);
      zdialog_stuff(zd,"shangle",line[ii].attr.shangle);
      return;
   }

   focus = "";                                                                   //  no selection
   focusii = -1;
   zd = zdimage;
   if (! zd) return;
   zdialog_stuff(zd,"currfile","*********");                                     //  mark obvious no selection
   return;
}


//  Set the update area for the layout based on
//  the current selected image, text, or line/arrow.

void mashup::setlayoutupdatearea()
{
   using namespace mashup;

   int         pxlo, pxhi, pylo, pyhi;
   int         ii, px, py, px0, py0;
   int         ww, hh, ww2, hh2;
   float       cosT, sinT;

   Fupdall = 0;

   if (strmatch(focus,"image"))
   {
      ii = focusii;
      px0 = image[ii].px0;
      py0 = image[ii].py0;
      ww2 = image[ii].ww2;                                                       //  image size at scale
      hh2 = image[ii].hh2;
      cosT = image[ii].cosT;
      sinT = image[ii].sinT;

      pxlo = pxhi = px0;                                                         //  NW corner
      pylo = pyhi = py0;

      px = px0 + ww2 * cosT;                                                     //  NE
      if (px < pxlo) pxlo = px;
      if (px > pxhi) pxhi = px;
      py = py0 + ww2 * sinT;
      if (py < pylo) pylo = py;
      if (py > pyhi) pyhi = py;

      px = px0 + ww2 * cosT - hh2 * sinT;                                        //  SE
      if (px < pxlo) pxlo = px;
      if (px > pxhi) pxhi = px;
      py = py0 + hh2 * cosT + ww2 * sinT;
      if (py < pylo) pylo = py;
      if (py > pyhi) pyhi = py;

      px = px0 - hh2 * sinT;                                                     //  SW
      if (px < pxlo) pxlo = px;
      if (px > pxhi) pxhi = px;
      py = py0 + hh2 * cosT;
      if (py < pylo) pylo = py;
      if (py > pyhi) pyhi = py;

      if (pxlo < 0) pxlo = 0;
      if (pxhi > Lww) pxhi = Lww;
      if (pylo < 0) pylo = 0;
      if (pyhi > Lhh) pyhi = Lhh;

      updxlo = image[ii].pxlo = pxlo;
      updxhi = image[ii].pxhi = pxhi;
      updylo = image[ii].pylo = pylo;
      updyhi = image[ii].pyhi = pyhi;
   }

   else if (strmatch(focus,"text"))
   {
      ii = focusii;
      px0 = text[ii].px0;
      py0 = text[ii].py0;
      ww = text[ii].ww;
      hh = text[ii].hh;

      updxlo = text[ii].pxlo = px0;                                              //  set update region
      updxhi = text[ii].pxhi = px0 + ww;
      updylo = text[ii].pylo = py0;
      updyhi = text[ii].pyhi = py0 + hh;
   }

   else if (strmatch(focus,"line"))
   {
      ii = focusii;
      px0 = line[ii].px0;
      py0 = line[ii].py0;
      ww = line[ii].ww;
      hh = line[ii].hh;

      updxlo = line[ii].pxlo = px0;                                              //  set update region
      updxhi = line[ii].pxhi = px0 + ww;
      updylo = line[ii].pylo = py0;
      updyhi = line[ii].pyhi = py0 + hh;
   }

   return;
}


//  mouse function - called for mouse events inside layout image
//  move and resize the overlay images, text images, line/arrow images

void mashup::mousefunc_layout()
{
   using namespace mashup;

   static int  mdx0 = 0, mdy0 = 0, mdx1, mdy1;
   static int  Mcen, Mcor, Tcen, Tcor, Lcen, Lcor;                               //  mouse at image/text/line center/corner
   static int  iiMcen, iiTcen, iiLcen, iiMcor, iiTcor, iiLcor;                   //  selected image/text/line center/corner
   static int  Tcorx, Tcory;
   int         cenx, ceny;
   int         minMcen, minMcor, minTcen, minTcor, minLcen, minLcor;
   int         ii, Fflash, Fmove, Fnew;
   int         mx, my, mx0, my0;
   int         ww, hh, ww2, hh2;
   int         pxlo, pxhi, pylo, pyhi;
   int         length;
   float       dist, distx, disty, Cdist, Mdist, Mdist0;
   float       expand, contract;
   float       scale, thresh, size;
   float       px0, py0, px1, py1, px2, py2, px3, py3;
   float       tx0, ty0, tx1, ty1, tx2, ty2, tx3, ty3;
   float       Vcx, Vcy, Vmx, Vmy;
   float       sinT, cosT;
   float       leng2, amx, amy, d1, d2, angle, rad;

   if (LMclick)                                                                  //  left click
   {
      LMclick = 0;
      Fnextimage = 0;
      mx0 = mx = Mxclick;                                                        //  mouse position
      my0 = my = Myclick;
      Fnew = 1;                                                                  //  new image or text selected
      Fmove = 0;
      Fflash = 1;
      select("",-1);                                                             //  unselect prior
   }

   else if (Mxdrag || Mydrag)                                                    //  drag underway
   {
      Fnew = 0;                                                                  //  assume same image/text as before
      Fmove = 1;                                                                 //    is being moved
      Fflash = 0;

      if (Mxdown != mdx0 || Mydown != mdy0) {                                    //  new drag was initiated
         mdx0 = mdx1 = Mxdown;
         mdy0 = mdy1 = Mydown;
         Fnew = 1;                                                               //  new image or text selected
      }

      mx0 = mdx1;                                                                //  drag start
      my0 = mdy1;
      mx = Mxdrag;                                                               //  drag position
      my = Mydrag;
      mdx1 = mx;                                                                 //  next drag start
      mdy1 = my;
      if (mx == mx0 && my == my0) Fmove = 0;                                     //  no mouse movement

      Mxdrag = Mydrag = 0;                                                       //  stop main window drag after return
   }

   else return;                                                                  //  ignore simple mouse movement

   if (Fnextimage) {                                                             //  image set from [next] button
      Fnextimage = 0;
      Fnew = 0;
      Mcen = 1;
      iiMcen = focusii;
   }

   if (zdaddtext && (Mxclick + Myclick)) {                                       //  return clicked position
      zdaddtext->zstat = 1;
      zdialog_free(zdaddtext);
      return;
   }

   if (zdaddline && (Mxclick + Myclick)) {                                       //  return clicked position
      zdaddline->zstat = 1;
      zdialog_free(zdaddline);
      return;
   }

   thresh = 200;                                                                 //  corner grab max. distance

   if (Fnew)                                                                     //  search all images and text images
   {
      minMcen = 9999;                                                            //  find closest image center to mouse

      for (ii = 0; ii < Nimage; ii++)
      {
         px0 = image[ii].px0;                                                    //  image position and rotation
         py0 = image[ii].py0;                                                    //    in layout image space
         sinT = image[ii].sinT;
         cosT = image[ii].cosT;
         ww2 = image[ii].ww2;                                                    //  image size at scale
         hh2 = image[ii].hh2;
         px2 = (mx-px0) * cosT + (my-py0) * sinT;                                //  image pixel for layout position
         py2 = (my-py0) * cosT - (mx-px0) * sinT;
         if (px2 < 0 || px2 > ww2-1) continue;                                   //  mouse not within image
         if (py2 < 0 || py2 > hh2-1) continue;
         pxlo = image[ii].pxlo;                                                  //  image extent
         pxhi = image[ii].pxhi;
         pylo = image[ii].pylo;
         pyhi = image[ii].pyhi;
         cenx = (pxlo + pxhi) / 2;                                               //  image center
         ceny = (pylo + pyhi) / 2;
         distx = mx - cenx;
         disty = my - ceny;
         dist = sqrtf(distx * distx + disty * disty);                            //  mouse/center distance
         if (dist < minMcen) {
            minMcen = dist;
            iiMcen = ii;                                                         //  save closest image center
         }
      }

      minMcor = 9999;                                                            //  find closest image corner to mouse

      for (ii = 0; ii < Nimage; ii++)
      {
         px0 = image[ii].px0;                                                    //  image position and rotation
         py0 = image[ii].py0;                                                    //    in layout image space
         sinT = image[ii].sinT;
         cosT = image[ii].cosT;
         ww2 = image[ii].ww2;                                                    //  image size at scale
         hh2 = image[ii].hh2;
         px2 = (mx-px0) * cosT + (my-py0) * sinT;                                //  image pixel for layout position
         py2 = (my-py0) * cosT - (mx-px0) * sinT;
         if (px2 < 0 || px2 > ww2-1) continue;                                   //  mouse not within image
         if (py2 < 0 || py2 > hh2-1) continue;

         px0 = image[ii].px0;                                                    //  NW corner
         py0 = image[ii].py0;
         distx = mx - px0;
         disty = my - py0;
         dist = sqrtf(distx * distx + disty * disty);                            //  mouse/corner distance
         if (dist < minMcor && dist < thresh) {
            minMcor = dist;
            iiMcor = ii;                                                         //  save closest image corner
         }

         px1 = px0 + ww2 * cosT;                                                 //  NE
         py1 = py0 + ww2 * sinT;
         distx = mx - px1;
         disty = my - py1;
         dist = sqrtf(distx * distx + disty * disty);
         if (dist < minMcor && dist < thresh) {
            minMcor = dist;
            iiMcor = ii;
         }

         px2 = px0 + ww2 * cosT - hh2 * sinT;                                    //  SE
         py2 = py0 + hh2 * cosT + ww2 * sinT;
         distx = mx - px2;
         disty = my - py2;
         dist = sqrtf(distx * distx + disty * disty);
         if (dist < minMcor && dist < thresh) {
            minMcor = dist;
            iiMcor = ii;
         }

         px3 = px0 - hh2 * sinT;                                                 //  SW
         py3 = py0 + hh2 * cosT;
         distx = mx - px3;
         disty = my - py3;
         dist = sqrtf(distx * distx + disty * disty);
         if (dist < minMcor && dist < thresh) {
            minMcor = dist;
            iiMcor = ii;
         }
      }

      minTcen = 9999;                                                            //  find closest text image center to mouse

      for (ii = 0; ii < Ntext; ii++)
      {
         pxlo = text[ii].pxlo;                                                   //  image extent
         pxhi = text[ii].pxhi;
         pylo = text[ii].pylo;
         pyhi = text[ii].pyhi;
         if (mx < pxlo || mx > pxhi) continue;                                   //  mouse not within text image
         if (my < pylo || my > pyhi) continue;

         cenx = (pxlo + pxhi) / 2;                                               //  text image center
         ceny = (pylo + pyhi) / 2;
         distx = mx - cenx;
         disty = my - ceny;
         dist = sqrtf(distx * distx + disty * disty);                            //  mouse/center distance
         if (dist < minTcen) {
            minTcen = dist;
            iiTcen = ii;
         }
      }

      minTcor = 9999;                                                            //  find closest text image corner to mouse
      Tcorx = Tcory = 0;

      for (ii = 0; ii < Ntext; ii++)
      {
         pxlo = text[ii].pxlo;                                                   //  text image extent
         pxhi = text[ii].pxhi;
         pylo = text[ii].pylo;
         pyhi = text[ii].pyhi;
         if (mx < pxlo || mx > pxhi) continue;                                   //  mouse not within text image
         if (my < pylo || my > pyhi) continue;

         ww = text[ii].attr.tww;                                                 //  unrotated text rectangle size
         hh = text[ii].attr.thh;
         sinT = text[ii].attr.sinT;                                              //  angle of text
         cosT = text[ii].attr.cosT;
         px0 = text[ii].px0;                                                     //  NW corner of enclosing rectangle
         py0 = text[ii].py0;

         if (text[ii].attr.angle > 0) {
            tx0 = px0 + hh * sinT;                                               //  NW corner of text rectangle
            ty0 = py0;
            tx1 = px0 + ww * cosT + hh * sinT;                                   //  NE
            ty1 = py0 + ww * sinT;
            tx2 = px0 + ww * cosT;                                               //  SE
            ty2 = py0 + ww * sinT + hh * cosT;
            tx3 = px0;                                                           //  SW
            ty3 = py0 + hh * cosT;
         }
         else {
            sinT = -sinT;
            tx0 = px0;                                                           //  NW
            ty0 = py0 + ww * sinT;
            tx1 = px0 + ww * cosT;                                               //  NE
            ty1 = py0;
            tx2 = px0 + ww * cosT + hh * sinT;                                   //  SE
            ty2 = py0 + hh * cosT;
            tx3 = px0 + hh * sinT;                                               //  SW
            ty3 = py0 + ww * sinT + hh * cosT;
         }

         distx = mx - tx0;                                                       //  NW corner
         disty = my - ty0;
         dist = sqrtf(distx * distx + disty * disty);                            //  mouse/corner distance
         if (dist < minTcor && dist < thresh) {
            minTcor = dist;
            iiTcor = ii;
            Tcorx = tx0;                                                         //  save closest text image corner
            Tcory = ty0;
         }

         distx = mx - tx1;                                                       //  NE
         disty = my - ty1;
         dist = sqrtf(distx * distx + disty * disty);
         if (dist < minTcor && dist < thresh) {
            minTcor = dist;
            iiTcor = ii;
            Tcorx = tx1;
            Tcory = ty1;
         }

         distx = mx - tx2;                                                       //  SE
         disty = my - ty2;
         dist = sqrtf(distx * distx + disty * disty);
         if (dist < minTcor && dist < thresh) {
            minTcor = dist;
            iiTcor = ii;
            Tcorx = tx2;
            Tcory = ty2;
         }

         distx = mx - tx3;                                                       //  SW
         disty = my - ty3;
         dist = sqrtf(distx * distx + disty * disty);
         if (dist < minTcor && dist < thresh) {
            minTcor = dist;
            iiTcor = ii;
            Tcorx = tx3;
            Tcory = ty3;
         }
      }

      minLcen = 9999;                                                            //  find closest line/arrow image
                                                                                 //     center to mouse position
      for (ii = 0; ii < Nline; ii++)
      {
         pxlo = line[ii].pxlo;                                                   //  image extent
         pxhi = line[ii].pxhi;
         pylo = line[ii].pylo;
         pyhi = line[ii].pyhi;
         if (mx < pxlo || mx > pxhi) continue;                                   //  mouse not within line image
         if (my < pylo || my > pyhi) continue;

         cenx = (pxlo + pxhi) / 2;                                               //  line image center
         ceny = (pylo + pyhi) / 2;
         distx = mx - cenx;
         disty = my - ceny;
         dist = sqrtf(distx * distx + disty * disty);                            //  mouse/center distance
         if (dist < minLcen) {
            minLcen = dist;
            iiLcen = ii;
         }
      }

      minLcor = 9999;                                                            //  find closest line end point to mouse

      for (ii = 0; ii < Nline; ii++)
      {
         pxlo = line[ii].pxlo;                                                   //  line image extent
         pxhi = line[ii].pxhi;
         pylo = line[ii].pylo;
         pyhi = line[ii].pyhi;
         if (mx < pxlo || mx > pxhi) continue;                                   //  mouse not within line image
         if (my < pylo || my > pyhi) continue;

         angle = line[ii].attr.angle;
         rad = -angle / 57.296;

         length = line[ii].attr.length;
         leng2 = length / 2.0;

         ww2 = line[ii].attr.pxb_line->ww;                                       //  line image
         hh2 = line[ii].attr.pxb_line->hh;

         amx = ww2 / 2.0;                                                        //  line midpoint within line image
         amy = hh2 / 2.0;

         px1 = amx - leng2 * cosf(rad) + 0.5;                                    //  line end points
         py1 = amy + leng2 * sinf(rad) + 0.5;
         px2 = amx + leng2 * cosf(rad) + 0.5;
         py2 = amy - leng2 * sinf(rad) + 0.5;

         d1 = (mx-px1-pxlo) * (mx-px1-pxlo) + (my-py1-pylo) * (my-py1-pylo);
         d2 = (mx-px2-pxlo) * (mx-px2-pxlo) + (my-py2-pylo) * (my-py2-pylo);

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

         if (d1 < minLcor && d1 < thresh) {
            minLcor = d1;
            iiLcor = ii;
         }

         if (d2 < minLcor && d2 < thresh) {
            minLcor = d2;
            iiLcor = ii;
         }
      }

      Mcen = Mcor = Tcen = Tcor = Lcen = Lcor = 0;                               //  mark where mouse is acting:

      if (Fmove)                                                                 //  recognize drags from anywhere
      {
         if (minMcen < minMcor && minMcen < minTcen && minMcen < minTcor && minMcen < minLcen && minMcen < minLcor)
            Mcen = 1;
         if (minMcor < minMcen && minMcor < minTcen && minMcor < minTcor && minMcor < minLcen && minMcor < minLcor)
            Mcor = 1;
         if (minTcen < minMcen && minTcen < minMcor && minTcen < minTcor && minTcen < minLcen && minTcen < minLcor)
            Tcen = 1;
         if (minTcor < minMcen && minTcor < minMcor && minTcor < minTcen && minTcor < minLcen && minTcor < minLcor)
            Tcor = 1;
         if (minLcen < minMcen && minLcen < minMcor && minLcen < minTcen && minLcen < minTcor && minLcen < minLcor)
            Lcen = 1;
         if (minLcor < minMcen && minLcor < minMcor && minLcor < minTcen && minLcor < minTcor && minLcor < minLcen)
            Lcor = 1;
      }
      else                                                                       //  recognize clicks near center only
      {
         if (minMcen < minTcen && minMcen < minLcen)
            Mcen = 1;
         if (minTcen < minMcen && minTcen < minLcen)
            Tcen = 1;
         if (minLcen < minMcen && minLcen < minTcen)
            Lcen = 1;
      }
   }     //  Fnew

   if (Mcen) { ii = iiMcen; select("image",ii); }                                //  select the corresp. image, text or line
   if (Mcor) { ii = iiMcor; select("image",ii); }
   if (Tcen) { ii = iiTcen; select("text",ii); }
   if (Tcor) { ii = iiTcor; select("text",ii); }
   if (Lcen) { ii = iiLcen; select("line",ii); }
   if (Lcor) { ii = iiLcor; select("line",ii); }

   if (Fflash) {
      if (Mcen) flashimage();                                                    //  if click on image, flash it
      if (Tcen) flashtext();                                                     //  same for text
      if (Lcen) flashline();                                                     //  same for line
      return;
   }

   if (Fmove)
   {                                                                             //  before changes (why needed?)
      setlayoutupdatearea();                                                     //  set layout update area
      Lupdate(21);                                                               //  update layout composite image

      if (Mcen)                                                                  //  move image
      {
         ii = iiMcen;
         if (mx0 < image[ii].pxlo || mx0 > image[ii].pxhi) return;               //  mouse outside image bounds
         if (my0 < image[ii].pylo || my0 > image[ii].pyhi) return;
         select("image",ii);
         if (image[ii].fixed) return;                                            //  ignore fixed image
         image[ii].px0 += (mx - mx0);                                            //  move selected image
         image[ii].py0 += (my - my0);                                            //    by mouse drag amount
      }

      if (Mcor)                                                                  //  resize image by dragging a corner
      {
         ii = iiMcor;
         select("image",ii);
         if (image[ii].fixed) return;                                            //  ignore fixed image

         pxlo = image[ii].pxlo;
         pxhi = image[ii].pxhi;
         pylo = image[ii].pylo;
         pyhi = image[ii].pyhi;
         cenx = (pxlo + pxhi) / 2;                                               //  image center
         ceny = (pylo + pyhi) / 2;

         distx = mx0 - cenx;
         disty = my0 - ceny;
         Mdist0 = sqrtf(distx * distx + disty * disty);                          //  center - mouse drag start
         distx = mx - cenx;
         disty = my - ceny;
         Mdist = sqrtf(distx * distx + disty * disty);                           //  center - mouse drag end

         scale = image[ii].scale * 0.5 * (Mdist + Mdist0) / Mdist0;              //  image scale change
         image_rescale(ii,scale);
      }

      if (Tcen)                                                                  //  move text image
      {
         ii = iiTcen;
         if (mx0 < text[ii].pxlo || mx0 > text[ii].pxhi) return;                 //  mouse outside text bounds
         if (my0 < text[ii].pylo || my0 > text[ii].pyhi) return;
         select("text",ii);
         text[ii].px0 += (mx - mx0);                                             //  move selected text image
         text[ii].py0 += (my - my0);                                             //    by mouse drag amount
      }

      if (Tcor)                                                                  //  resize text image
      {
         ii = iiTcor;
         select("text",ii);

         pxlo = text[ii].pxlo;
         pxhi = text[ii].pxhi;
         pylo = text[ii].pylo;
         pyhi = text[ii].pyhi;
         cenx = (pxlo + pxhi) / 2;                                               //  text image center
         ceny = (pylo + pyhi) / 2;
         distx = Tcorx - cenx;
         disty = Tcory - ceny;
         Cdist = sqrtf(distx * distx + disty * disty);
         Vcx = distx / Cdist;                                                    //  unit vector, text image center to corner
         Vcy = disty / Cdist;

         distx = mx - mx0;
         disty = my - my0;
         Mdist = sqrtf(distx * distx + disty * disty);
         Vmx = distx / Mdist;                                                    //  unit vector, mouse movement
         Vmy = disty / Mdist;

         expand   = fabsf(Vcx + Vmx) + fabsf(Vcy + Vmy);                         //  mouse pull-out amount
         contract = fabsf(Vcx - Vmx) + fabsf(Vcy - Vmy);                         //  push-in amount
         size = expand - contract;                                               //  change in text size
         if (size > 0.5) text[ii].attr.size++;
         if (size < -0.5) text[ii].attr.size--;

         gentext(&text[ii].attr);                                                //  generate new text image
         text[ii].ww = text[ii].attr.pxb_text->ww;
         text[ii].hh = text[ii].attr.pxb_text->hh;
      }

      if (Lcen)                                                                  //  move line/arrow image
      {
         ii = iiLcen;
         if (mx0 < line[ii].pxlo || mx0 > line[ii].pxhi) return;                 //  mouse outside line bounds
         if (my0 < line[ii].pylo || my0 > line[ii].pyhi) return;
         select("line",ii);
         line[ii].px0 += (mx - mx0);                                             //  move selected line image
         line[ii].py0 += (my - my0);                                             //    by mouse drag amount
      }

      if (Lcor)                                                                  //  drag line end point
      {
         ii = iiLcor;
         if (mx0 < line[ii].pxlo || mx0 > line[ii].pxhi) return;                 //  mouse outside line bounds
         if (my0 < line[ii].pylo || my0 > line[ii].pyhi) return;
         select("line",ii);

         angle = line[ii].attr.angle;
         rad = -angle / 57.296;

         length = line[ii].attr.length;
         leng2 = length / 2.0;

         ww2 = line[ii].attr.pxb_line->ww;                                       //  line image
         hh2 = line[ii].attr.pxb_line->hh;

         amx = ww2 / 2.0;                                                        //  line midpoint within line image
         amy = hh2 / 2.0;

         px1 = amx - leng2 * cosf(rad) + 0.5;                                    //  line end points
         py1 = amy + leng2 * sinf(rad) + 0.5;
         px2 = amx + leng2 * cosf(rad) + 0.5;
         py2 = amy - leng2 * sinf(rad) + 0.5;

         pxlo = line[ii].pxlo;
         pylo = line[ii].pylo;

         d1 = (mx-px1-pxlo) * (mx-px1-pxlo) + (my-py1-pylo) * (my-py1-pylo);
         d2 = (mx-px2-pxlo) * (mx-px2-pxlo) + (my-py2-pylo) * (my-py2-pylo);

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

         if (d1 < d2) {                                                          //  move px1/py1 end to mouse
            px2 += pxlo;
            py2 += pylo;
            px1 = mx;
            py1 = my;
            length = d2 + 0.5;
            rad = asinf((py1-py2) / d2);
            angle = -57.296 * rad;
            if (mx > px2) angle = -180 - angle;
         }

         if (d2 < d1) {                                                          //  move px2/py2 end to mouse
            px1 += pxlo;
            py1 += pylo;
            px2 = mx;
            py2 = my;
            length = d1 + 0.5;
            rad = asinf((py1-py2) / d1);
            angle = -57.296 * rad;
            if (mx < px1) angle = -180 - angle;
         }

         if (angle < -180) angle += 360;
         if (angle > 180) angle -= 360;
         line[ii].attr.angle = angle;
         line[ii].attr.length = length;
         genline(&line[ii].attr);

         line[ii].ww = line[ii].attr.pxb_line->ww;
         line[ii].hh = line[ii].attr.pxb_line->hh;
         ww2 = line[ii].attr.pxb_line->ww;
         hh2 = line[ii].attr.pxb_line->hh;
         amx = (px1 + px2) / 2.0;
         amy = (py1 + py2) / 2.0;
         pxlo = amx - ww2 / 2.0;
         pylo = amy - hh2 / 2.0;
         line[ii].px0 = pxlo;
         line[ii].py0 = pylo;
      }
   }        //  Fmove

   setlayoutupdatearea();
   Lupdate(21);                                                                  //  update layout composite image
   return;
}


//  Keyboard function
//  KB arrow keys act like mouse drag of 1 pixel for current image

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

   int      ii, xstep, ystep;

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

   if (strmatch(focus,"image"))                                                  //  choose last selected image
   {
      ii = focusii;
      if (image[ii].fixed) return;                                               //  ignore fixed image
      image[ii].px0 += xstep;                                                    //  move image 1 pixel
      image[ii].py0 += ystep;
   }

   else if (strmatch(focus,"text"))                                              //  last selected text image
   {
      ii = focusii;
      text[ii].px0 += xstep;                                                     //  move selected text image 1 pixel
      text[ii].py0 += ystep;
   }

   else if (strmatch(focus,"line"))                                              //  last selected line/arrow image
   {
      ii = focusii;
      line[ii].px0 += xstep;                                                     //  move selected line image 1 pixel
      line[ii].py0 += ystep;
   }

   else return;

   setlayoutupdatearea();                                                        //  set layout update area
   Lupdate(22);                                                                  //  update layout composite image
   return;
}


//  update layout image Fpxb from layout + overlay images
//  paint main window

void mashup::Lupdate(int caller)
{
   using namespace mashup;

   int            nupdxlo, nupdxhi, nupdylo, nupdyhi;
   static int     pupdxlo, pupdxhi, pupdylo, pupdyhi;                            //  prior image update region
   int            ww, hh;
   static int     busy = 0;

   if (! Lpxb || ! Fpxb) return;

   if (busy++) return;                                                           //  stop re-entrance

   if (updxlo < 0) updxlo = 0;                                                   //  keep within layout image
   if (updxhi > Lww) updxhi = Lww;
   if (updylo < 0) updylo = 0;
   if (updyhi > Lhh) updyhi = Lhh;

   nupdxlo = updxlo;                                                             //  save update region
   nupdxhi = updxhi;
   nupdylo = updylo;
   nupdyhi = updyhi;

   if (! Fupdall) {
      if (pupdxlo < updxlo) updxlo = pupdxlo;                                    //  expand update region to
      if (pupdxhi > updxhi) updxhi = pupdxhi;                                    //    include prior update region
      if (pupdylo < updylo) updylo = pupdylo;
      if (pupdyhi > updyhi) updyhi = pupdyhi;
   }

   pupdxlo = nupdxlo;                                                            //  save update region
   pupdxhi = nupdxhi;
   pupdylo = nupdylo;
   pupdyhi = nupdyhi;

   if (Fupdall) {                                                                //  update entire layout
      updxlo = updylo = 0;
      updxhi = Lww;
      updyhi = Lhh;
   }

   do_wthreads(Lupdate_wthread,NSMP);                                            //  worker threads

   if (Fupdall) Fpaintnow();                                                     //  paint entire layout
   else {
      ww = updxhi-updxlo;
      hh = updyhi-updylo;
      PXB_PXB_update(Fpxb,Mpxb,updxlo,updylo,ww,hh);                             //  Fpxb > Mpxb, scaled up or down
      Fpaint4(updxlo,updylo,ww,hh,0);                                            //  update drawing window from Mpxb
   }

   Fupdall = 0;
   busy = 0;
   return;
}


void * mashup::Lupdate_wthread(void *arg)                                        //  worker thread for images
{
   using namespace mashup;

   int      index = *((int *) (arg));
   int      ii, jj, kk, px1, py1, ww, hh, qx, qy;
   uint8    *pix1, *pix2, *pix3, vpix2[4];
   float    red, green, blue;
   float    Bopac, Iopac, Vtran;
   float    px0, py0, px2, py2;
   float    ww1, hh1, ww2, hh2;
   float    edgedist, margin, blendist;
   float    scale, sinT, cosT;
   PXB      *pxb2;
   float    e3part;

   for (py1 = updylo+index; py1 < updyhi; py1 += NSMP)                           //  loop layout image pixels
   for (px1 = updxlo; px1 < updxhi; px1++)                                       //    within update region
   {
      pix1 = PXBpix(Lpxb,px1,py1);                                               //  start with layout pixel

      red = pix1[0];                                                             //  background RGB
      green = pix1[1];
      blue = pix1[2];

      for (ii = 0; ii < Nimage; ii++)                                            //  loop overlay images from bottom to top
      {
         if (image[ii].pxhi < updxlo) continue;                                  //  no overlap with update region
         if (image[ii].pxlo > updxhi) continue;
         if (image[ii].pyhi < updylo) continue;
         if (image[ii].pylo > updyhi) continue;

         px0 = image[ii].px0;                                                    //  overlay image position and rotation
         py0 = image[ii].py0;                                                    //    in layout image space
         ww2 = image[ii].ww2;                                                    //  scaled image size
         hh2 = image[ii].hh2;

         if (image[ii].theta == 0) {                                             //  image not rotated
            px2 = px1 - px0;                                                     //  overlay image pixel for layout pixel
            py2 = py1 - py0;
         }
         else {
            sinT = image[ii].sinT;                                               //  image is rotated
            cosT = image[ii].cosT;
            px2 = (px1-px0) * cosT + (py1-py0) * sinT;                           //  overlay image pixel for layout pixel
            py2 = (py1-py0) * cosT - (px1-px0) * sinT;
         }
         if (px2 < 0 || px2 > ww2-1) continue;                                   //  layout pixel not in overlay image
         if (py2 < 0 || py2 > hh2-1) continue;

         pxb2 = image[ii].pxb2;                                                  //  scaled overlay image
         if (! vpixel(pxb2,px2,py2,vpix2)) continue;                             //  virt. pixel is beyond the edge

         Bopac = 1.0 - image[ii].Btransp;                                        //  image base opacity 0...1
         Iopac = vpix2[3] / 255.0;                                               //  alpha channel opacity 0...1
         Iopac = Iopac * Bopac;                                                  //  combined

         scale = image[ii].scale;

         if (image[ii].vtranmap) {                                               //  image has painted transparency        24.60
            ww1 = image[ii].ww1;                                                 //  image size at 1x scale
            hh1 = image[ii].hh1;
            qx = px2 / scale;                                                    //  position in 1x image
            qy = py2 / scale;
            if (image[ii].warpcc) {                                              //  scaled image is warped
               kk = qy * ww1 + qx;
               qx += image[ii].warpx[kk];                                        //  source pixel in 1x image
               qy += image[ii].warpy[kk];
            }
            if (qx < ww1 && qy < hh1) {                                          //  position is within image
               jj = qy * ww1 + qx;
               Vtran = image[ii].vtranmap[jj] / 255.0;                           //  var. transparency 0...1
               Iopac = Iopac * (1 - Vtran);
            }
         }

         if (image[ii].Lmarg || image[ii].Lblend) {                              //  left edge hard or blend margin
            edgedist = px2 / scale;
            margin = image[ii].Lmarg;
            if (edgedist <= margin) Iopac = 0;
            else {
               edgedist -= margin;
               blendist = image[ii].Lblend;
               if (edgedist < blendist)
                  Iopac = Iopac * edgedist / blendist;
            }
         }

         if (image[ii].Rmarg || image[ii].Rblend) {                              //  right edge
            edgedist = (ww2 - px2) / scale;
            margin = image[ii].Rmarg;
            if (edgedist <= margin) Iopac = 0;
            else {
               edgedist -= margin;
               blendist = image[ii].Rblend;
               if (edgedist < blendist)
                  Iopac = Iopac * edgedist / blendist;
            }
         }

         if (image[ii].Tmarg || image[ii].Tblend) {                              //  top edge
            edgedist = py2 / scale;
            margin = image[ii].Tmarg;
            if (edgedist <= margin) Iopac = 0;
            else {
               edgedist -= margin;
               blendist = image[ii].Tblend;
               if (edgedist < blendist)
                  Iopac = Iopac * edgedist / blendist;
            }
         }

         if (image[ii].Bmarg || image[ii].Bblend) {                              //  bottom edge
            edgedist = (hh2 - py2) / scale;
            margin = image[ii].Bmarg;
            if (edgedist <= margin) Iopac = 0;
            else {
               edgedist -= margin;
               blendist = image[ii].Bblend;
               if (edgedist < blendist)
                  Iopac = Iopac * edgedist / blendist;
            }
         }

         red = Iopac * vpix2[0] + (1 - Iopac) * red;                             //  add image pixel over prior
         green = Iopac * vpix2[1] + (1 - Iopac) * green;
         blue = Iopac * vpix2[2] + (1 - Iopac) * blue;
      }

      for (ii = 0; ii < Ntext; ii++)                                             //  loop overlay text images
      {
         if (text[ii].pxhi < updxlo) continue;                                   //  no overlap with update region
         if (text[ii].pxlo > updxhi) continue;
         if (text[ii].pyhi < updylo) continue;
         if (text[ii].pylo > updyhi) continue;

         px0 = text[ii].px0;                                                     //  text image position in layout space
         py0 = text[ii].py0;
         ww = text[ii].ww;                                                       //  text image size in layout
         hh = text[ii].hh;
         qx = px1 - px0;                                                         //  text image pixel for layout pixel
         qy = py1 - py0;
         if (qx < 0 || qx > ww-1) continue;                                      //  layout pixel not in this text image
         if (qy < 0 || qy > hh-1) continue;

         pxb2 = text[ii].attr.pxb_text;
         pix2 = PXBpix(pxb2,qx,qy);
         e3part = pix2[3] / 255.0;

         red = pix2[0] + e3part * red;
         green = pix2[1] + e3part * green;
         blue = pix2[2] + e3part * blue;
      }

      for (ii = 0; ii < Nline; ii++)                                             //  loop overlay line/arrow images
      {
         if (line[ii].pxhi < updxlo) continue;                                   //  no overlap with update region
         if (line[ii].pxlo > updxhi) continue;
         if (line[ii].pyhi < updylo) continue;
         if (line[ii].pylo > updyhi) continue;

         px0 = line[ii].px0;                                                     //  line image position in layout space
         py0 = line[ii].py0;
         ww = line[ii].ww;                                                       //  line image size in layout
         hh = line[ii].hh;
         qx = px1 - px0;                                                         //  line image pixel for layout pixel
         qy = py1 - py0;
         if (qx < 0 || qx > ww-1) continue;                                      //  layout pixel not in this line image
         if (qy < 0 || qy > hh-1) continue;

         pxb2 = line[ii].attr.pxb_line;
         pix2 = PXBpix(pxb2,qx,qy);
         e3part = pix2[3] / 255.0;

         red = pix2[0] + e3part * red;
         green = pix2[1] + e3part * green;
         blue = pix2[2] + e3part * blue;
      }

      pix3 = PXBpix(Fpxb,px1,py1);                                               //  output image pixel
      pix3[0] = red;
      pix3[1] = green;
      pix3[2] = blue;
   }

   return 0;                                                                     //  exit thread
}


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

   IMAGE ARRAY

   Combine selected images into a array or matrix format within one image.

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

namespace image_array_names
{
   int         Tabww;                     //  array width (input)
   int         Tabhh;                     //  array height (calculated)
   int         Tabmarg;                   //  array margin size (input)
   uint8       Tabbg[3];                  //  array background color
   int         Ncols;                     //  image columns in array (input)
   int         Nrows;                     //  image rows in tablle (calculated)
   int         Ihsep, Ivsep;              //  image separation, horz and vert
   int         Iww, Ihh;                  //  image width, height without margins

   typedef struct {
      int      random;                    //  random number for sorting
      int      Ihh;                       //  image height (calculated, variable)
      int      Ipx, Ipy;                  //  image position in array image
      int      row, col;                  //  image row and column number
      ch       *file;                     //  image filename
      PIXBUF   *pixbuf;                   //  scaled image pixbuf
   }  image_t;

   #define maxNm 1000                     //  max. images in array
   image_t     image[maxNm];              //  image data
   int         Nimages;                   //  actual image count

   int         maxNc = 100;               //  max. columns
   int         maxNr = 100;               //  max. rows
   int         colH[100];                 //  column heights

   ch        uniquename[100];             //  unique array file name
   ch        arraypath[500];              //  suggested folder to save file
}


//  menu function

void m_image_array(GtkWidget *, ch *)
{
   using namespace image_array_names;

   int  image_array_dialog_event(zdialog* zd, ch *event);
   int  image_array_mapimages(void);
   int  image_array_showarray(void);
   int  image_array_sort(void);
   int  image_array_shuffle(void);
   int  image_array_spread(void);
   int  image_array_mapfile(void);

   int      ii, nn, err, zstat, ww, hh, makemap;
   int      coldiff, coldiffB, coldiff2;
   image_t  imageB[maxNm];
   ch       text[100], *filename, *pp, *pp1;
   float    R;
   PXB      *pxb1, *pxb2;

   F1_help_topic = "image array";

   Plog(1,"m_image_array \n");

   if (Fblock("image array")) return;

   Nimages = 0;

/***
       ______________________________________
      |            Image Array               |
      |                                      |
      |  [Select Files]  NN files selected   |
      |  Background Color [######]           |
      |  Array Width [___]  Margin [___]     |
      |  Image Columns [___]                 |
      |  Separation Horz [___]  Vert [___]   |
      |                                      |
      |                        [Proceed] [X] |
      |______________________________________|

***/

   zdialog *zd = zdialog_new("Image Array",Mwin,"Proceed"," X ",null);

   //  [Select Files]  NN files selected
   zdialog_add_widget(zd,"hbox","hbf","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","files","hbf","Select Files","space=5");
   zdialog_add_widget(zd,"label","labfcount","hbf","no files selected","space=10");

   //  Background Color [######]
   zdialog_add_widget(zd,"hbox","hbtbg","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labtbg","hbtbg","Background Color","space=3");
   zdialog_add_widget(zd,"colorbutt","Tabbg","hbtbg","255|255|255");

   //  Array Width [___]  Margin [___]
   zdialog_add_widget(zd,"hbox","hbf","dialog");
   zdialog_add_widget(zd,"label","labfw","hbf","Array Width","space=3");
   zdialog_add_widget(zd,"zspin","Tabww","hbf","100|30000|1|2000","space=3");
   zdialog_add_widget(zd,"label","space","hbf",0,"space=5");
   zdialog_add_widget(zd,"label","labfm","hbf","Margin","space=3");
   zdialog_add_widget(zd,"zspin","Tabmarg","hbf","0|500|1|30","space=3");

   //  Image Columns [___]
   zdialog_add_widget(zd,"hbox","hbcols","dialog");
   zdialog_add_widget(zd,"label","labcols","hbcols","Image Columns","space=3");
   zdialog_add_widget(zd,"zspin","Ncols","hbcols","1|100|1|4","space=3");        //  max 100 columns

   //  Separation Horz [___]  Vert [___]
   zdialog_add_widget(zd,"hbox","hbsep","dialog");
   zdialog_add_widget(zd,"label","labhsep","hbsep","Separation Horz","space=3");
   zdialog_add_widget(zd,"zspin","Ihsep","hbsep","0|500|1|20","space=3");
   zdialog_add_widget(zd,"label","space","hbsep",0,"space=5");
   zdialog_add_widget(zd,"label","labvsep","hbsep","Vert","space=3");
   zdialog_add_widget(zd,"zspin","Ivsep","hbsep","0|500|1|20","space=3");

   Tabbg[0] = Tabbg[1] = Tabbg[2] = 200;                                         //  default background color: off white

   zdialog_load_inputs(zd);                                                      //  restore previous inputs
   zdialog_run(zd,image_array_dialog_event,"save");                              //  run dialog - parallel

   zstat = zdialog_wait(zd);                                                     //  wait for completion
   if (zstat != 1) goto cleanup;

   zdialog_free(zd);
   zd = 0;

   Nrows = 1 + (SFcount - 1) / Ncols;                                            //  required image rows
   if (Nrows > maxNr) {
      zmessageACK(Mwin,"exceed %d rows",maxNr);
      goto cleanup;
   }

   if (SFcount < Ncols) Ncols = SFcount;                                         //  reduce Ncols if fewer images

   Iww = (Tabww - 2 * Tabmarg - (Ncols-1) * Ihsep) / Ncols;                      //  scaled image width

   Funcbusy(+1,1);

   for (nn = ii = 0; ii < SFcount; ii++)                                         //  loop all images
   {
      zmainloop();

      image[nn].file = 0;
      image[nn].Ihh = 0;
      image[nn].Ipx = 0;
      image[nn].Ipy = 0;
      image[nn].row = 0;
      image[nn].col = 0;
      image[nn].pixbuf = 0;

      image[nn].file = zstrdup(SelFiles[ii],"array");                            //  image file name

      image[nn].row = nn / Ncols;                                                //  initial image row
      image[nn].col = nn - Ncols * image[nn].row;                                //  initial image column

      pxb1 = PXB_load(image[nn].file,1);                                         //  load PXB from file
      if (! pxb1) continue;

      PXB_subalpha(pxb1);                                                        //  remove alpha channel

      ww = pxb1->ww;                                                             //  image dimensions
      hh = pxb1->hh;
      R = 1.0 * Iww / ww;                                                        //  image scale ratio
      image[nn].Ihh = R * hh + 0.5;                                              //  scaled image height

      pxb2 = PXB_rescale(pxb1,Iww,image[nn].Ihh);                                //  rescale image  higher quality

      image[nn].pixbuf = gdk_pixbuf_copy(pxb2->pixbuf);                          //  copy image pixbuf

      PXB_free(pxb1);
      PXB_free(pxb2);

      nn++;                                                                      //  accumulate image count
   }

   Funcbusy(-1);

   Nimages = nn;                                                                 //  final image count
   if (! Nimages) {
      zmessageACK(Mwin,"no images were found");
      goto cleanup;
   }

   Nrows = 1 + (Nimages - 1) / Ncols;                                            //  final row count

   strncpy0(arraypath,image[0].file,500);                                        //  first selected image file
   pp = strrchr(arraypath,'/');                                                  //  use this folder as default
   if (pp) *pp = 0;                                                              //    array file output location

   coldiff = image_array_mapimages();                                            //  map scaled images into array image
   err = image_array_showarray();                                                //  show completed array image
   if (err) goto optdone;

   for (ii = 0; ii < Nimages; ii++)                                              //  best map = initial map
      imageB[ii] = image[ii];
   coldiffB = coldiff;
   if (coldiff < 3) {
      zstat = 2;
      goto optdone;
   }

/***
          ___________________________________
         |          Optimize                 |
         |                                   |
         |  column difference: NNN pixels    |
         |                                   |
         |                [Start] [Stop] [X] |
         |___________________________________|

***/

   zd = zdialog_new("Optimize",Mwin,"Start","Stop"," X ",null);
   zdialog_add_widget(zd,"label","labdiff","dialog","stuffed");
   zdialog_set_modal(zd);
   zdialog_run(zd,null,"save");                                                  //  run dialog
   zstat = 0;                                                                    //  status = not started

   Funcbusy(+1,1);

   while (true)                                                                  //  optimization loop
   {
      snprintf(text,100,"column difference: %d pixels",coldiffB);                //  show best column diff. so far
      zdialog_stuff(zd,"labdiff",text);
      zmainloop();

      if (zd->zstat) {                                                           //  dialog status change
         zstat = zd->zstat;                                                      //  new status
         zd->zstat = 0;                                                          //  keep dialog active
      }

      if (zstat == 0) {                                                          //  not started
         zsleep(0.1);
         continue;                                                               //  wait for start
      }

      if (zstat < 0 || zstat >= 2) break;                                        //  [x]

      while (true)                                                               //  [start] or continue
      {
         coldiff2 = coldiff;                                                     //  shuffle() until no improvement
         image_array_shuffle();
         coldiff = image_array_mapimages();
         if (coldiff >= coldiff2) break;
      }

      if (coldiff < coldiffB) {
         for (ii = 0; ii < Nimages; ii++)                                        //  save new best map
            imageB[ii] = image[ii];
         coldiffB = coldiff;
         err = image_array_showarray();                                          //  show completed array image
         zmainsleep(1);
         if (err) break;
      }

      if (coldiff < 3) {                                                         //  stop if column diff. is tiny
         zstat = 2;
         break;
      }

      image_array_sort();                                                        //  re-sort all images
      coldiff = image_array_mapimages();                                         //  compute new column difference
   }

   Funcbusy(-1);

optdone:

   if (zstat != 2) goto cleanup;                                                 //  cancel

   zdialog_free(zd);
   zd = 0;

   for (ii = 0; ii < Nimages; ii++)                                              //  restore best result
      image[ii] = imageB[ii];

   coldiff = image_array_mapimages();

   if (coldiff > 1) {
      int yn = zmessageYN(Mwin,"column difference: %d pixels \n"
                               "Make columns even?",coldiff);
      if (yn) {
         image_array_mapimages();
         image_array_spread();
         image_array_showarray();
      }
   }

/***
          _________________________________________
         |   Save with unique image array name     |
         |                                         |
         | unique name: [________________________] |
         |                                         |
         | [x] create image map                    |
         |                                         |
         |                              [Save] [X] |
         |_________________________________________|

***/

   zd = zdialog_new("Save with unique image array name",Mwin,"Save"," X ",null);
   zdialog_add_widget(zd,"hbox","hbname","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labname","hbname","unique name:","space=5");
   zdialog_add_widget(zd,"zentry","uniquename","hbname","array","size=30");
   zdialog_add_widget(zd,"check","createmap","dialog","create image map","space=10");

   pp = strrchr(image[0].file,'/');                                              //  default first file name               24.30
   snprintf(uniquename,100,"array: %s",pp+1);
   pp = strrchr(uniquename,'.');
   if (pp) *pp = 0;
   zdialog_stuff(zd,"uniquename",uniquename);

   zdialog_run(zd,null,"save");

   while (true)
   {
      zstat = zdialog_wait(zd);
      if (zstat != 1) break;
      zdialog_fetch(zd,"uniquename",uniquename,86);                              //  get unique array file name
      if (strlen(uniquename) > 3) break;
      zmessageACK(Mwin,"supply a reasonable name");
      zd->zstat = 0;
   }

   if (zstat != 1) goto cleanup;                                                 //  user canceled

   strncatv(arraypath,500,"/",uniquename,".jpg",0);                              //  .../uniquename.jpg

   filename = zgetfile("save array",MWIN,"save",arraypath,0);                    //  save array file, user choice
   if (! filename) goto cleanup;
   err = f_save(filename,"jpg",8,0,1);
   if (err) goto cleanup;

   f_open(filename);                                                             //  make it the current file

   pp1 = strrchr(filename,'/');                                                  //  get actual file name used
   if (! pp1) goto cleanup;
   strncpy0(uniquename,pp1+1,100);
   zfree(filename);

   zdialog_fetch(zd,"createmap",makemap);                                        //  create image map file?
   if (makemap) {
      err = image_array_mapfile();                                               //  create corresponding map file
      if (! err) zmessageACK(Mwin,"map file saved: %s",uniquename);
   }

cleanup:
   if (zd) zdialog_free(zd);                                                     //  kill dialog
   zd = 0;
   for (ii = 0; ii < Nimages; ii++)
      g_object_unref(image[ii].pixbuf);                                          //  free memory
   gallery(arraypath,"init",0);
   gallery(image[0].file,"paint",0);
   viewmode('F');
   Fblock(0); 
   return;
}


//  array dialog event and completion function

int image_array_dialog_event(zdialog *zd, ch *event)
{
   using namespace image_array_names;

   ch        countmess[80];
   ch        colorbutt[20];
   ch        *pp;

   if (strmatch(event,"files"))                                                  //  select images to process
   {
      zdialog_show(zd,0);
      select_files(0,1);                                                         //  get file list                         24.20
      snprintf(countmess,80,"%d image files selected",SFcount);
      zdialog_stuff(zd,"labfcount",countmess);
      zdialog_show(zd,1);
   }

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

   zd->zstat = 0;                                                                //  keep dialog active

   if (SFcount == 0) {                                                           //  no files selected
      zmessageACK(Mwin,"no files selected");
      return 1;
   }

   if (SFcount > maxNm) {
      zmessageACK(Mwin,"%d max images exceeded",maxNm);
      return 1;
   }

   zdialog_fetch(zd,"Tabbg",colorbutt,19);                                       //  get background color
   pp = substring(colorbutt,"|",1);
   if (pp) Tabbg[0] = atoi(pp);
   pp = substring(colorbutt,"|",2);
   if (pp) Tabbg[1] = atoi(pp);
   pp = substring(colorbutt,"|",3);
   if (pp) Tabbg[2] = atoi(pp);

   zdialog_fetch(zd,"Tabww",Tabww);                                              //  array width
   zdialog_fetch(zd,"Tabmarg",Tabmarg);                                          //  array margin
   zdialog_fetch(zd,"Ncols",Ncols);                                              //  image columns
   zdialog_fetch(zd,"Ihsep",Ihsep);                                              //  image horz spacing
   zdialog_fetch(zd,"Ivsep",Ivsep);                                              //  image vert spacing

   zd->zstat = 1;                                                                //  dialog finished
   return 1;
}


//  Map scaled image positions into array image.
//  Calculate max. difference in image column height.

int image_array_mapimages()
{
   using namespace image_array_names;

   int      ii, jj;
   int      maxcol, mincol;
   int      row, col, bott;

   for (ii = 0; ii < Nimages; ii++)                                              //  loop all images
   {
      row = image[ii].row;                                                       //  image row
      col = image[ii].col;                                                       //  image column
      jj = ii - Ncols;                                                           //  prior image in same column
      image[ii].Ipx = Tabmarg + col * (Iww + Ihsep);                             //  image x position
      if (row == 0) image[ii].Ipy = Tabmarg;                                     //  image y position for row 0
      else image[ii].Ipy = image[jj].Ipy + image[jj].Ihh + Ivsep;                //  image y position for row 1+
   }

   maxcol = 0;
   mincol = 999999;

   for (ii = Nimages - Ncols; ii < Nimages; ii++)                                //  loop last Ncols images
   {
      bott = image[ii].Ipy + image[ii].Ihh;                                      //  image y position + height
      if (bott > maxcol) maxcol = bott;                                          //  save max. bottom edge
      if (bott < mincol) mincol = bott;                                          //  save min. bottom edge
      col = image[ii].col;
      colH[col] = bott;                                                          //  save column height
   }

   return (maxcol - mincol);                                                     //  return column height difference
}


//  fill the array image from scaled images and show the array image

int image_array_showarray()
{
   using namespace image_array_names;

   uint8    *Fpixels, *pixel;
   int      ii, px, py, Frs, col, maxcol = 0;
   int      ww, hh, st, bott, err;
   ch       *file;
   PIXBUF   *Fpixbuf, *pixbuf;
   GError   *gerror = 0;

   for (ii = Nimages - Ncols; ii < Nimages; ii++)                                //  loop last Ncols images
   {
      bott = image[ii].Ipy + image[ii].Ihh;                                      //  image y position + height
      if (bott > maxcol) maxcol = bott;                                          //  save max. bottom edge
      col = image[ii].col;
      colH[col] = bott;                                                          //  save column height
   }

   Tabhh = maxcol + Tabmarg;                                                     //  set array image height

   if (! edit_wwhhOK(Tabww,Tabhh)) return 1;

   Fpixbuf = gdk_pixbuf_new(GDKRGB,0,8,Tabww,Tabhh);                             //  make array image pixbuf
   if (! Fpixbuf) {
      zmessageACK(Mwin,"array width is too large: %d x %d",Tabww,Tabhh);
      return 1;                                                                  //  error return
   }

   Fpixels = gdk_pixbuf_get_pixels(Fpixbuf);                                     //  fill array image with background color
   Frs = gdk_pixbuf_get_rowstride(Fpixbuf);

   for (py = 0; py < Tabhh; py++)
   for (px = 0; px < Tabww; px++)
   {
      pixel = Fpixels + py * Frs + px * 3;
      pixel[0] = Tabbg[0];
      pixel[1] = Tabbg[1];
      pixel[2] = Tabbg[2];
   }

   for (ii = 0; ii < Nimages; ii++)                                              //  loop all images
   {
      pixbuf = image[ii].pixbuf;                                                 //  copy scaled image into array image
      ww = gdk_pixbuf_get_width(pixbuf);
      hh = gdk_pixbuf_get_height(pixbuf);
      gdk_pixbuf_copy_area(pixbuf,0,0,ww,hh,Fpixbuf,image[ii].Ipx,image[ii].Ipy);
      zmainloop();
   }

   file = zstrdup(temp_folder,"image_array",30);                                 //  save array image to temp file
   strcat(file,"/image (image array).jpg");
   st = gdk_pixbuf_save(Fpixbuf,file,"jpeg",&gerror,"quality","90",null);
   if (! st) {
      zmessageACK(Mwin,"GDK pixbuf: cannot save file");
      zfree(file);
      g_object_unref(Fpixbuf);
      return 2;                                                                  //  error return
   }

   viewmode('F');
   err = f_open(file);                                                           //  show array image
   if (err) return 3;

   zfree(file);
   g_object_unref(Fpixbuf);

   return 0;
}


//  shuffle image column assignments to make column heights even

int image_array_shuffle()
{
   using namespace image_array_names;

   int      ii, jj, col, colii, coljj;
   int      hh1, hh2, diff, maxdiff;
   int      bestii, bestjj;
   image_t  tempimage;

   for (ii = Nimages - Ncols; ii < Nimages; ii++)                                //  loop last Ncols images
   {
      col = image[ii].col;                                                       //  get column height
      colH[col] = image[ii].Ipy + image[ii].Ihh;                                 //    = y position + image height
   }

   for (ii = 0; ii < Nimages; ii++)                                              //  loop all images ii
   {
      zmainloop();

      colii = image[ii].col;
      maxdiff = bestii = 0;

      for (jj = 0; jj < Nimages; jj++)                                           //  loop all images jj
      {
         coljj = image[jj].col;
         if (colii == coljj ) continue;                                          //  both images in same column

         hh1 = colH[colii] - image[ii].Ihh + image[jj].Ihh;                      //  column heights if images are switched
         hh2 = colH[coljj] + image[ii].Ihh - image[jj].Ihh;
         diff = abs(colH[colii] - colH[coljj]) - abs(hh1 - hh2);                 //  column height diff - switched diff
         if (diff > maxdiff) {
            maxdiff = diff;                                                      //  best switch for nearest heights
            bestii = ii;
            bestjj = jj;
         }
      }

      if (maxdiff > 0) {
         ii = bestii;                                                            //  switch the images
         jj = bestjj;

         col = image[ii].col;
         colH[col] = colH[col] - image[ii].Ihh + image[jj].Ihh;
         col = image[jj].col;
         colH[col] = colH[col] + image[ii].Ihh - image[jj].Ihh;

         tempimage = image[ii];
         image[ii] = image[jj];
         image[jj] = tempimage;

         image[jj].row = image[ii].row;
         image[jj].col = image[ii].col;
         image[ii].row = tempimage.row;
         image[ii].col = tempimage.col;
      }
   }

   return 0;
}


//  sort images randomly to get a new starting point for shuffle

int image_array_sort()
{
   using namespace image_array_names;

   int      ii;
   int      keys[1][3] = { { 0, 4, 3 } };                                        //  key position, length, type

   for (ii = 0; ii < Nimages; ii++)                                              //  populate random sort key
      image[ii].random = 1000000 * drandz();

   MemSort((ch *) image, sizeof(image_t), Nimages, keys, 1);                     //  sort image[]

   for (ii = 0; ii < Nimages; ii++)
   {
      image[ii].row = ii / Ncols;                                                //  new image row, column
      image[ii].col = ii - Ncols * image[ii].row;
   }

   return 1;
}


//  spread images in short columns to make column heights even.

int image_array_spread()
{
   using namespace image_array_names;

   int      col, row, nrows, ii, dy;
   int      maxH = 0, diff, diff2;

   if (Nrows < 2) return 0;

   for (ii = Nimages - Ncols; ii < Nimages; ii++)                                //  loop last Ncols images
   {
      col = image[ii].col;                                                       //  get column height
      colH[col] = image[ii].Ipy + image[ii].Ihh;                                 //    = y position + image height
   }

   for (col = 0; col < Ncols; col++)                                             //  get max. column height
      if (colH[col] > maxH) maxH = colH[col];

   for (col = 0; col < Ncols; col++)                                             //  loop all columns
   {
      diff = maxH - colH[col];
      if (diff < 2) continue;                                                    //  column height near max, no change

      nrows = Nrows;                                                             //  max. images per column
      ii = (nrows - 1) * Ncols + col;                                            //  last image[ii] this column
      if (ii >= Nimages) nrows -= 1;                                             //  this is a short column
      if (nrows < 2) break;
      diff2 = diff / (nrows - 1);                                                //  increase for Y-posn after row 0

      for (dy = row = 0; row < nrows; row++)                                     //  loop all rows in column
      {
         ii = row * Ncols + col;
         if (ii == Nimages) break;
         image[ii].Ipy += dy;
         dy += diff2;                                                            //  prior increase + per row increase
      }
   }

   return 0;
}


//  create map file - maps the scaled image areas in the image_array file
//    to the corresponding image files

int image_array_mapfile()
{
   using namespace image_array_names;

   int      ii, lox, hix, loy, hiy;
   float    flox, fhix, floy, fhiy;
   ch       arraymapfile[300];
   FILE     *fid;

   snprintf(arraymapfile,300,"%s/%s",image_array_maps_folder,uniquename);

   fid = fopen(arraymapfile,"w");
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return 1;
   }

   for (ii = 0; ii < Nimages; ii++)
   {
      lox = image[ii].Ipx;                                                       //  scaled image pixel range
      loy = image[ii].Ipy;
      hix = lox + Iww;
      hiy = loy + image[ii].Ihh;

      flox = 1.0 * lox / Tabww;                                                  //  rescale to 0.0 - 1.0
      floy = 1.0 * loy / Tabhh;                                                  //  (immune to image resize)
      fhix = 1.0 * hix / Tabww;
      fhiy = 1.0 * hiy / Tabhh;

      fprintf(fid,"%.5f %.5f %.5f %.5f  file: %s\n",
                     flox,floy,fhix,fhiy,image[ii].file);
   }

   fclose(fid);

   return 0;
}


//  mouse function
//  show the image file corresponding to clicked position in image array file

void image_array_Lclick_func(int mousex, int mousey)
{
   using namespace image_array_names;

   typedef struct {
      ch       *file;                                                            //  image filename
      int      lox, loy;                                                         //  scaled image area in array image
      int      hix, hiy;
   }  image_t;

   image_t     image[maxNm];                                                     //  image data

   int         fww, fhh, Nimages = 0;
   ch          buff[500], arraymapfile[300];
   ch          *pp, *pp1, *file;
   int         ii, nn;
   float       flox, fhix, floy, fhiy;
   FILE        *fid;

   fww = Fpxb->ww;                                                               //  array image dimensions
   fhh = Fpxb->hh;

   pp1 = strrchr(curr_file,'/');                                                 //  extract array unique name
   if (! pp1) goto notfound;
   strncpy0(uniquename,pp1+1,100);

   snprintf(arraymapfile,300,"%s/%s",image_array_maps_folder,uniquename);        //  corresp. array map file

   fid = fopen(arraymapfile,"r");
   if (! fid) goto notfound;                                                     //  not found

   for (ii = 0; ii < maxNm; ii++)                                                //  loop map file recs
   {
      pp = fgets_trim(buff,500,fid,1);
      if (! pp) break;
      nn = sscanf(buff,"%f %f %f %f",&flox,&floy,&fhix,&fhiy);
      if (nn != 4) goto invalid;

      pp = strstr(buff,"file: ");
      if (! pp) goto invalid;

      image[ii].file = zstrdup(pp + 6,"image_array");
      image[ii].lox = flox * fww;
      image[ii].loy = floy * fhh;
      image[ii].hix = fhix * fww;
      image[ii].hiy = fhiy * fhh;
   }

   fclose(fid);
   Nimages = ii;
   if (Nimages < 2) goto invalid;

   for (ii = 0; ii < Nimages; ii++)
   {
      if (mousex <= image[ii].lox) continue;
      if (mousex >= image[ii].hix) continue;
      if (mousey <= image[ii].loy) continue;
      if (mousey >= image[ii].hiy) continue;
      break;
   }

   if (ii == Nimages) goto freemem;

   file = image[ii].file;
   if (! regfile(file)) {
      zmessageACK(Mwin,"file not found: %s",file);
      goto freemem;
   }

   popup_image(file,MWIN,1,512);
   goto freemem;

notfound:
   zmessageACK(Mwin,"file not found: %s",uniquename);
   goto freemem;

invalid:
   zmessageACK(Mwin,"image array map file invalid: %s",uniquename);
   goto freemem;

freemem:
   for (ii = 0; ii < Nimages; ii++)
      if (image[ii].file) zfree(image[ii].file);

   return;
}
