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

   Fotoxx      edit photos and manage collections

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

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

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

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

   Fotoxx image edit functions - Mashup and Montage functions

   m_mashup       combine images and text into custom layout
   m_montage      combine images into a matrix of images

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

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

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

   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
{
   char        projectname[100];                                                 //  project name
   char        projectfile[200];                                                 //  /.../projectname
   char        layoutfile[XFCC];                                                 //  background/layout image file
   char        tranfile[220];                                                    //  /.../projectname_trandata
   char        warpfile[220];                                                    //  /.../projectname_warpdata
   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
   int         Fupdatebusy;                                                      //  window update busy

   cchar       *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
      char     *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      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
      int      Nwarp;                                                            //  warpmem count
      float    warpmem[200][5];                                                  //  warp undo memory, last 200 warps
      int      warpcc;                                                           //  warpx/warpy data length
      float    *warpx, *warpy;                                                   //  aggregate pixel warps
   };

   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 50                                                           //  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, cchar *event);
   int   project_open(void);
   void  project_save(void);
   void  project_rescale(float scale);

   void  image_edit(void);
   int   image_dialog_event(zdialog *zd, cchar *event);
   void  image_rescale(int ii, float scale2);
   void  paintransp_dialog(void);
   int   paintransp_dialog_event(zdialog *zd, cchar *event);
   void  paintransp_mousefunc(void);
   void  warp_dialog(void);
   int   warp_dialog_event(zdialog *zd, cchar *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, cchar *event);
   void  add_text(void);
   void  remove_text(int ii);
   void  flashtext(void);

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

   void  select(cchar *which, int ii);
   void  setlayoutupdatearea(void);
   void  mousefunc_layout(void);
   void  KBfunc(int key);
   void  Lupdate(void);
   void  * Lupdate_thread(void *);
   void  * Lupdate_wthread(void *arg);
}


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

//  menu function

void m_mashup(GtkWidget *, cchar *)
{
   using namespace mashup;
   
   zdialog     *zd;
   int         zstat, nn, err, ww, hh;
   char        color[20], *file;
   cchar       *choice = 0, *ppc;
   int         px, py;
   uint8       *pix;

   F1_help_topic = "mashup";

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0
   if (checkpend("all")) return;
   Fblock = 1;

   *projectname = *layoutfile = *projectfile = *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 [_______________________] |                                //  20.0
      |                                         |
      |  Layout and background image            |
      |  (o) choose an image file               |
      |  (o) use current image file             |
      |  (o) specify layout size and color      |
      |  (o) open a Mashup project file         |
      |                                         |
      |                     [Proceed] [Cancel]  |
      |_________________________________________|

***/

   zd = zdialog_new("Mashup",Mwin,Bproceed,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hbpn","dialog",0,"space=1");
   zdialog_add_widget(zd,"label","labpn","hbpn",E2X("Project name"),"space=3");
   zdialog_add_widget(zd,"zentry","projectname","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",E2X("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",E2X("choose an image file"));
   zdialog_add_widget(zd,"radio","usecurrent","vbopt",E2X("use current image file"));
   zdialog_add_widget(zd,"radio","makelayout","vbopt",E2X("specify layout size and color"));
   zdialog_add_widget(zd,"radio","openproject","vbopt",E2X("open a Mashup project file"));

   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,"projectname",projectname,100);                              //  get project name                   20.0
   strTrim2(projectname);
   if (*projectname <= ' ') {
      zmessageACK(Mwin,E2X("enter a project name"));
      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 = gallery_select1(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) {
         zmessageACK(Mwin,E2X("no current file"));
         goto try_again;
      }
      file = zstrdup(curr_file);
      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 [____]                                 |
               |                               [done] [cancel] |
               |_______________________________________________|

      ***/

      zd = zdialog_new(E2X("Make Layout Image"),Mwin,Bdone,Bcancel,null);
      zdialog_add_widget(zd,"hbox","hbz","dialog",0,"space=1");
      zdialog_add_widget(zd,"label","labw","hbz",Bwidth,"space=5");
      zdialog_add_widget(zd,"zspin","width","hbz","1000|20000|1|2000");          //  limit 20K x 10K = 200 megapixel PXB
      zdialog_add_widget(zd,"label","space","hbz",0,"space=5");                  //  (x 12 = 2.4 GB for PXM image)
      zdialog_add_widget(zd,"label","labh","hbz",Bheight,"space=5");
      zdialog_add_widget(zd,"zspin","height","hbz","600|10000|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",Bcolor,"space=5");
      zdialog_add_widget(zd,"colorbutt","color","hbc","200|200|200");

      zdialog_restore_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 = strField(color,"|",1);
      if (ppc) flatRGB[0] = atoi(ppc);
      ppc = strField(color,"|",2);
      if (ppc) flatRGB[1] = atoi(ppc);
      ppc = strField(color,"|",3);
      if (ppc) flatRGB[2] = atoi(ppc);

      zdialog_free(zd);

      Lpxb = PXB_make(ww,hh,3);                                                  //  layout image
      if (! Lpxb) return;
      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:
   Fblock = 0;                                                                   //  restart mashup
   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
   m_viewmode(0,"F");                                                            //  force file view

   PRscale = 1.0;
   select("",-1);                                                                //  nothing selected
   Fupdall = 1;
   Lupdate();                                                                    //  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      |
      |  [Done]          project complete          |
      |  [Cancel]        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",E2X("Edit Images"),"space=3");
   zdialog_add_widget(zd,"button","edittext","vb1",E2X("Edit Text"),"space=3");
   zdialog_add_widget(zd,"button","editline","vb1",E2X("Edit Line"),"space=3");
   zdialog_add_widget(zd,"button","rescale","vb1",E2X("Rescale"),"space=3");
   zdialog_add_widget(zd,"button","done","vb1",Bdone,"space=3");
   zdialog_add_widget(zd,"button","cancel","vb1",Bcancel,"space=3");
   
   zdialog_add_widget(zd,"hbox","hbedim","vb2");
   zdialog_add_widget(zd,"label","labedim","hbedim",E2X("add or edit images"),"space=3");
   zdialog_add_widget(zd,"hbox","hbedtx","vb2");
   zdialog_add_widget(zd,"label","labedtx","hbedtx",E2X("add or edit text"),"space=3");
   zdialog_add_widget(zd,"hbox","hbedln","vb2");
   zdialog_add_widget(zd,"label","labedln","hbedln",E2X("add or edit lines/arrows"),"space=3");
   zdialog_add_widget(zd,"hbox","hbres","vb2");
   zdialog_add_widget(zd,"label","labscale","hbres",E2X("change project scale"),"space=3");
   zdialog_add_widget(zd,"hbox","hbdone","vb2");
   zdialog_add_widget(zd,"label","labdone","hbdone",E2X("project complete"),"space=3");
   zdialog_add_widget(zd,"hbox","hbcan","vb2");
   zdialog_add_widget(zd,"label","labcancel","hbcan",E2X("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, cchar *event)
{
   using namespace mashup;

   int      ii, cc, yn;
   GError   *gerror = 0;
   char     *file = 0, *file2, *pp;
   cchar    *rescalemess = E2X("rescale project");
   
   if (zd->zstat) event = "cancel";                                              //  20.0

   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",Breset,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 == GDIR) {                                           //  20.0
         file2 = zstrdup(navi::galleryname,108);
         cc = strlen(file2);
         strncatv(file2,cc+108,"/",projectname,".png",null);                     //  gallery/projectname.png
      }
      else {
         file2 = zstrdup(projectname,6);                                         //  projectname.png
         strcat(file2,".png");
      }

      file = zgetfile(E2X("save Mashup output file"),MWIN,"save",file2);
      zfree(file2);
      if (file) {
         pp = strrchr(file,'.');                                                 //  force .png extension
         if (! pp || strlen(pp) < 4) {
            pp = zstrdup(file,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,E2X("save Mashup project file?"));                    //  offer to save project file
      if (yn) project_save();

      event = "cleanup";
   }

   if (strmatch(event,"cancel"))                                                 //  project canceled
   {
      yn = zmessageYN(Mwin,"cancel project?");                                   //  20.0
      if (! yn) return 1;
      
      zdialog_free(zd);                                                          //  kill dialog
      freeMouse();

      if (*projectfile) {
         yn = zmessageYN(Mwin,E2X("delete Mashup project file?"));               //  offer to delete project file
         if (yn) {
            remove(projectfile);
            if (*tranfile) remove(tranfile);
            if (*warpfile) remove(warpfile);
         }
      }

      m_viewmode(0,"G");                                                         //  20.0
      event = "cleanup";
   }

   if (strmatch(event,"cleanup"))
   {
      for (ii = Nimage-1; ii >= 0; ii--)                                         //  free memory
         remove_image(ii);
      for (ii = Ntext-1; ii >= 0; ii--)
         remove_text(ii);
      for (ii = Nline-1; ii >= 0; ii--)
         remove_line(ii);

      PXB_free(Lpxb);
      Lpxb = 0;

      Fmashup = 0;
      Fblock = 0;

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

   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, err;
   int         ww1, hh1;
   int         px, py, kk;
   int         nfid = -1, cc, wcc, vcc;
   char        *pp, *pp2;
   char        buff[XFCC];
   textattr_t  *txattr = 0;
   lineattr_t  *lnattr = 0;
   FILE        *fid;
   STATB       sbuff;
   uint8       *pix;

   pp = zgetfile(E2X("Open Project"),MWIN,"file",mashup_folder);                 //  get project file from user
   if (! pp) return 1;

   strncpy0(projectfile,pp,200);                                                 //  project file
   strncpy0(tranfile,pp,200);
   strcat(tranfile,"_trandata");                                                 //  corresp. transparencies file
   strncpy0(warpfile,pp,200);
   strcat(warpfile,"_warpdata");                                                 //  corresp. warp data file
   zfree(pp);

   pp = strrchr(projectfile,'/');                                                //  project name from file name
   strncpy0(projectname,pp+1,100);

   //  read project file and validate all data
   
   fid = fopen(projectfile,"r");                                                 //  open project file
   if (! fid) goto failure;

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

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

      strncpy0(layoutfile,pp,XFCC);
      err = stat(layoutfile,&sbuff);
      if (err) {
         zmessageACK(Mwin,E2X("layout image file missing: \n %s"),layoutfile);
         printz("no layout image \n"); 
         goto badproject;
      }

      Lpxb = PXB_load(layoutfile,1);                                             //  layout image
      if (! Lpxb) { printz("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) { printz("flatlayout rec. \n"); goto badproject; }

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

      Lpxb = PXB_make(ww1,hh1,3);                                                //  layout image
      if (! Lpxb) { printz("PXB_make() \n"); goto badproject; }

      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 { printz("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
      {
         ii = Nimage++;
         cc = sizeof(image_t);                                                   //  clear image data
         memset(&image[ii],0,cc);

         pp += 6;
         while (*pp == ' ') pp++;
         image[ii].file = zstrdup(pp);                                           //  overlay image file

         err = stat(image[ii].file,&sbuff);
         if (err) {
            zmessageACK(Mwin,E2X("overlay image file missing: \n %s"),image[ii].file);
            goto cleanupErr;
         }

         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) { printz("position rec. \n"); goto badproject; }
         continue;
      }

      if (strmatchN(pp,"scale:",6)) {                                            //  scale: n.nnn
         nn = sscanf(pp,"scale: %f",&image[ii].scale);
         if (nn != 1) { printz("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) { printz("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) { printz("Btransp rec. \n"); goto badproject; }
         continue;
      }

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

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

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

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

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

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

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

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

      if (strmatchN(pp,"Nwarp:",6)) {                                            //  Nwarp: nn
         nn = sscanf(pp,"Nwarp: %d",&image[ii].Nwarp);
         if (nn != 1) { printz("Nwarp rec. \n"); goto badproject; }
         if (image[ii].Nwarp > 200) { printz("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) { printz("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) { printz("vtrancc rec. \n"); goto badproject; }
         if (vcc && vcc != ww1 * hh1) { printz("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) { printz("warpcc rec. \n"); goto badproject; }
         if (wcc && wcc != (int) (ww1 * hh1 * sizeof(float))) { printz("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,"\\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) { printz("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) { printz("font rec. \n"); goto badproject; }
         *pp2 = 0;
         pp2 += 5;
         strncpy0(txattr->font,pp,80);
         txattr->size = atoi(pp2);
         if (txattr->size < 8) { printz("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) { printz("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) { printz("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) { printz("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);
            cc = read(nfid,image[ii].vtranmap,vcc);
            if (cc != vcc) { printz("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);
            cc = read(nfid,image[ii].warpx,wcc);
            if (cc != wcc) { printz("warpx data \n"); goto badproject; }
            image[ii].warpy = (float *) zmalloc(wcc);
            cc = read(nfid,image[ii].warpy,wcc);
            if (cc != wcc) { printz("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
   }

   m_viewmode(0,"F");
   return 0;                                                                     //  success return

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

badproject:                                                                      //  project file had a problem
   if (fid) fclose(fid);
   if (nfid > -1) close(nfid);
   zmessageACK(Mwin,E2X("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;
   char        *pp, text2[1200];
   FILE        *fid;
   int         nfid, ii, kk, cc, vcc, wcc;

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

   snprintf(projectfile,200,"%s/%s",mashup_folder,projectname);
   pp = zgetfile(E2X("Save Project"),MWIN,"save",projectfile);                   //  get project name from user
   if (! pp) return;

   strncpy0(projectfile,pp,200);                                                 //  project file
   strncpy0(tranfile,pp,200);
   strcat(tranfile,"_trandata");                                                 //  corresp. transparency data file
   strncpy0(warpfile,pp,200);
   strcat(warpfile,"_warpdata");                                                 //  corresp. warp data file
   zfree(pp);

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

   if (*layoutfile) fprintf(fid,"layout: %s\n",layoutfile);                      //  write layout file

   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
   {
      fprintf(fid,"image: %s\n",image[ii].file);
      fprintf(fid,"position: %d %d \n", image[ii].px0, image[ii].py0);
      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,"\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,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,E2X("layout exceeds 2 gigabytes"));
      return;
   }

   if (scale < 1) PRscale = 1.0;                                                 //  reset to 1x
   else PRscale = scale;                                                         //  resize 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();
   return;
}


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

//  mashup image edit function

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

   cchar    *tipmess = E2X("Click image to select, drag image to move.");
   cchar    *blackmargmess = E2X("Make black margins transparent");

   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           |
         |                                               |
         |  Margins      hard       blend                |
         |   Left       [ 123 ]    [   0 ]               |
         |   Right      [   0 ]    [ 123 ]               |
         |   Top        [ 234 ]    [   0 ]               |
         |   Bottom     [   0 ]    [ 234 ]               |
         |                                               |
         |                  [Add Image] [Delete] [Done]  |
         |_______________________________________________|

***/

   zdimage = zdialog_new("Edit Images",Mwin,E2X("Add Image"),Bdelete,Bdone,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",E2X("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",E2X("Cycle through images:"),"space=3");
   zdialog_add_widget(zd,"button","prev","hbnext",Bprev,"space=8");
   zdialog_add_widget(zd,"button","next","hbnext",Bnext);

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

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

   zdialog_add_widget(zd,"hbox","hbangle","dialog");
   zdialog_add_widget(zd,"label","labangle","hbangle",Bangle,"space=3");
   zdialog_add_widget(zd,"zspin","angle","hbangle","-180|180|0.1|0");

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

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

   zdialog_add_widget(zd,"hbox","hbvtranmap","dialog");
   zdialog_add_widget(zd,"label","labvtr","hbvtranmap",E2X("Var. Transparency"),"space=3");
   zdialog_add_widget(zd,"button","vtranmap","hbvtranmap",E2X("Paint"),"space=5");

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

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

   zdialog_add_widget(zd,"hbox","hbmarg","dialog");                              //  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",E2X("Margins"));
   zdialog_add_widget(zd,"label","labhard","vbmarg2",E2X("Hard"));
   zdialog_add_widget(zd,"label","labblend","vbmarg3",E2X("Blend"));
   zdialog_add_widget(zd,"label","lableft","vbmarg1",Bleft);
   zdialog_add_widget(zd,"label","labright","vbmarg1",Bright);
   zdialog_add_widget(zd,"label","labtop","vbmarg1",Btop);
   zdialog_add_widget(zd,"label","labbott","vbmarg1",Bbottom);
   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,E2X("Add Image"),E2X("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, cchar *event)
{
   using namespace mashup;

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

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

      zdimage = 0;

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

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

      return 1;
   }

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

      zdimage = 0;                                                               //  done or [x] kill

      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"))                                                  //  resize
   {
      zdialog_fetch(zd,"scale",scale);                                           //  new image scale
      image_rescale(ii,scale);
      select("image",ii);
   }

   if (strmatch(event,"scale1x"))                                                //  resize 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,"vtranmap")) {                                             //  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,"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();                                                                    //  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;

   cchar    *ptitle = E2X("Paint Image Transparencies");

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

***/

   zdtransp = zdialog_new(ptitle,Mwin,Bdone,null);
   zdialog_add_widget(zdtransp,"hbox","hbrad","dialog",0,"space=3");
   zdialog_add_widget(zdtransp,"label","labrad","hbrad",Bradius,"space=5");
   zdialog_add_widget(zdtransp,"zspin","radius","hbrad","5|300|1|100");
   zdialog_add_widget(zdtransp,"check","gradual","hbrad",E2X("Gradual"),"space=10");
   zdialog_add_widget(zdtransp,"hbox","hbpow","dialog",0,"space=3");
   zdialog_add_widget(zdtransp,"label","labpower","hbpow",E2X("Power"),"space=5");
   zdialog_add_widget(zdtransp,"label","labcen","hbpow",Bcenter,"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",Bedge,"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, cchar *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, 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, px0, py0, px1, py1;

   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;
   px1 = (mx-px0) * cosT + (my-py0) * sinT;                                      //  mouse position within image
   py1 = (my-py0) * cosT - (mx-px0) * sinT;

   if (px1+mrad < 0 || px1-mrad > ww2 ||                                         //  entire circle off image
       py1+mrad < 0 || py1-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);                               //    and one uint8 per pixel
      memset(image[ii].vtranmap,0,vcc);                                          //  initial values = 0 transparency
      image[ii].vtrancc = vcc;
   }

   qx = px1 / image[ii].scale;                                                   //  mouse position in 1x image
   qy = py1 / image[ii].scale;

   mrad = mrad / image[ii].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
         mtransp = Mpowcen * (mrad - dist) / mrad                                //  mouse power at distance from center
                 + Mpowedge * dist / mrad;                                       //    Mpowcen ... Mpowedge  scale 0-100
         if (drandz() < 0.005) continue;                                         //  thin active points
      }
      else mtransp = 255;                                                        //  instant paint transparency

      rx = qx + dx;                                                              //  1x 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();                                                                    //  update layout composite image
   return;
}


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

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


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

   cchar  *warp_message = E2X("Pull on the image with the mouse.");

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

***/

   zdwarp = zdialog_new(E2X("Warp Image"),Mwin,Bdone,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",Bundolast,"space=8");
   zdialog_add_widget(zdwarp,"button","undoall","hb1",Bundoall,"space=2");
   zdialog_add_widget(zdwarp,"hbox","hb2","dialog",0,"space=4");
   zdialog_add_widget(zdwarp,"label","lab2","hb2",E2X("warp span"),"space=8");
   zdialog_add_widget(zdwarp,"zspin","span","hb2","0.00|1.0|0.01|0.2","space=1");

   span = 0.2;
   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, cchar *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];
         span = image[ii].warpmem[kk][4];
         zdialog_stuff(zd,"span",span);
         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();                                                                 //  update layout image
   }

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

   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;

   scale = image[ii].scale;
   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);
      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;
         hh1 = image[ii].hh1;
         wcc = ww1 * hh1 * sizeof(float);
         image[ii].warpx = (float *) zmalloc(wcc);                               //  get memory for pixel displacements
         image[ii].warpy = (float *) zmalloc(wcc);
         image[ii].warpcc = wcc;
         memset(image[ii].warpx,0,wcc);
         memset(image[ii].warpy,0,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 * span;

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

   else if (Fdrag)                                                               //  drag complete
   {
      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,image[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] = span;
      image[ii].Nwarp++;
   }

   setlayoutupdatearea();                                                        //  update layout image
   Lupdate();
   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,NWT);                                       //  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 += NWT)                                      //  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 += NWT)                                      //  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;
      }
   }

   pthread_exit(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();                                                                    //  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;
   char        **selfiles = 0, *file;
   PXB         *pxb1;

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

   gallery_select_clear();                                                       //  clear gallery_select() file list
   gallery_select();                                                             //  select image files from gallery

   m_viewmode(0,"F");                                                            //  restore view mode F

   zdialog_show(zdimage,1);
   
   if (! GScount) return;                                                        //  nothing selected
   
   selfiles = (char **) zmalloc(GScount * sizeof(char *));

   for (ii = 0; ii < GScount; ii++)
      selfiles[ii] = GSfiles[ii];

   Nfiles = GScount;
   GScount = 0;

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

      file = selfiles[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(selfiles[nn++]);                                    //  free unused files
   zfree(selfiles);

   select("",-1);                                                                //  nothing selected
   Fupdall = 1;
   Lupdate();                                                                    //  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();                                                                    //  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
   zmainloop();
   zsleep(0.1);                                                                  //  hold for a moment

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


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

//  add text to image or revise existing text

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

   cchar    *intro = E2X("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] [Add] [Delete] [Done] |
      |__________________________________________________|

***/

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

   zdtext = zdialog_new(E2X("Edit Text"),Mwin,Bclear,Badd,Bdelete,Bdone,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",E2X("Text"),"space=5");
   zdialog_add_widget(zd,"frame","frtext","hbtext",0,"expand");
   zdialog_add_widget(zd,"edit","text","frtext","","expand");

   zdialog_add_widget(zd,"hbox","hbfont","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","fontbutt","hbfont",Bfont);
   zdialog_add_widget(zd,"zentry","fontname","hbfont","FreeSans","space=2|expand");
   zdialog_add_widget(zd,"label","labsize","hbfont",Bsize,"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",E2X("text"));
   zdialog_add_widget(zd,"label","labback","vbcol1",E2X("backing"));
   zdialog_add_widget(zd,"label","laboutln","vbcol1",E2X("outline"));
   zdialog_add_widget(zd,"label","labshadow","vbcol1",E2X("shadow"));

   zdialog_add_widget(zd,"label","labcol","vbcol2",Bcolor);
   zdialog_add_widget(zd,"colorbutt","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",Bwidth);
   zdialog_add_widget(zd,"label","space","vbcol4");
   zdialog_add_widget(zd,"label","space","vbcol4");
   zdialog_add_widget(zd,"zspin","towidth","vbcol4","0|30|1|0");
   zdialog_add_widget(zd,"zspin","shwidth","vbcol4","0|50|1|0");

   zdialog_add_widget(zd,"label","labw","vbcol5",Bangle);
   zdialog_add_widget(zd,"zspin","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",E2X("Text File:"),"space=3");
   zdialog_add_widget(zd,"button","loadtext","hbaf",Bopen,"space=5");
   zdialog_add_widget(zd,"button","savetext","hbaf",Bsave,"space=5");

   zdialog_add_ttip(zd,Badd,E2X("add entered text to layout"));

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

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

   if (strmatch(event,"kill")) {                                                 //  kill dialog
      zdialog_free(zd);
      zdtext = 0;
      return 1;
   }

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

      if (zd->zstat == 2) {                                                      //  add
         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;
      }

      zdtext = 0;                                                                //  done or [x] kill
      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(E2X("select font"),MWIN);
      gtk_font_chooser_set_font(GTK_FONT_CHOOSER(font_dialog),font);
      gtk_dialog_run(GTK_DIALOG(font_dialog));
      pp = gtk_font_chooser_get_font(GTK_FONT_CHOOSER(font_dialog));
      gtk_widget_destroy(font_dialog);

      if (pp) {                                                                  //  should have "fontname nn"
         strncpy0(font,pp,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();                                                                    //  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;
   cchar       *tip = E2X("click position to add text");

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

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

   Mxclick = Myclick = 0;
   zdaddtext = zdialog_new(E2X("Add Text"),Mwin,Bcancel,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();                                                                    //  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();                                                                    //  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;
   char        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(strField(text[ii].attr.color[jj],'|',1));
      green = 255 - atoi(strField(text[ii].attr.color[jj],'|',2));
      blue = 255 - atoi(strField(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();                                                                    //  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();                                                                    //  update layout composite image
   return;
}


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

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

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

   cchar    *intro = E2X("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] [Done] |
      |_______________________________________________|

***/

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

   zdline = zdialog_new(E2X("Edit Line/Arrow"),Mwin,Badd,Bdelete,Bdone,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",E2X("Line length"),"space=5");
   zdialog_add_widget(zd,"zspin","length","hbline","2|9999|1|20");
   zdialog_add_widget(zd,"label","space","hbline",0,"space=10");
   zdialog_add_widget(zd,"label","labwidth","hbline",Bwidth,"space=5");
   zdialog_add_widget(zd,"zspin","width","hbline","1|99|1|2");

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

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

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

   zdialog_add_widget(zd,"label","labcol","vbcol2",Bcolor);
   zdialog_add_widget(zd,"colorbutt","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",Bwidth);
   zdialog_add_widget(zd,"label","space","vbcol4");
   zdialog_add_widget(zd,"label","space","vbcol4");
   zdialog_add_widget(zd,"zspin","towidth","vbcol4","0|30|1|0");
   zdialog_add_widget(zd,"zspin","shwidth","vbcol4","0|50|1|0");

   zdialog_add_widget(zd,"label","labw","vbcol5",Bangle);
   zdialog_add_widget(zd,"zspin","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,Badd,E2X("add line/arrow to layout"));

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

   if (strmatch(event,"kill")) {                                                 //  kill dialog
      zdialog_free(zd);
      zdline = 0;
      return 1;
   }

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

      zdline = 0;                                                                //  done or [x] kill
      return 1;
   }

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

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

   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();                                                                    //  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, zstat;
   zdialog     *zd = zdline;   
   cchar       *tip = E2X("click position to add line");

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

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

   Mxclick = Myclick = 0;
   zdaddline = zdialog_new(E2X("Add Line"),Mwin,Bdone,Bcancel,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");
   zstat = zdialog_wait(zdaddline);
   zdialog_free(zdaddline);
   if (zstat != 1) return;
   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();                                                                    //  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();                                                                    //  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;
   char        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(strField(line[ii].attr.color[jj],'|',1));
      green = 255 - atoi(strField(line[ii].attr.color[jj],'|',2));
      blue = 255 - atoi(strField(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();                                                                    //  update layout composite image
   zsleep(0.05);                                                                 //  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();                                                                    //  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(cchar *which, int ii)
{
   using namespace mashup;

   char     *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,"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_destroy(zdaddtext);
      return;
   }

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

   thresh = 50;                                                                  //  corner match threshold, pixels     20.0

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

   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)
   {
      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);
         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;
//       if (mx0 < image[ii].pxlo || mx0 > image[ii].pxhi) return;               //  mouse outside image bounds
//       if (my0 < image[ii].pylo || my0 > image[ii].pyhi) return;               //  removed 20.0
         select("image",ii);

         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;
//       if (mx0 < text[ii].pxlo || mx0 > text[ii].pxhi) return;                 //  mouse outside text bounds
//       if (my0 < text[ii].pylo || my0 > text[ii].pyhi) return;                 //  removed 20.0
         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;
      }
   }

   setlayoutupdatearea();
   Lupdate();                                                                    //  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;
      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();                                                                    //  update layout composite image
   return;
}


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

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

   int            nupdxlo, nupdxhi, nupdylo, nupdyhi;
   static int     pupdxlo, pupdxhi, pupdylo, pupdyhi;                            //  prior image update region
   static cchar   *pfocus = "x";
   static int     pfocusii = -2;                                                 //  no prior selected image or text
   int            ww, hh;

   if (! Lpxb || ! Fpxb) return;

   if (! strmatch(focus,pfocus) || focusii != pfocusii)                          //  focus image/text/line has changed
      Fupdall = 1;                                                               //  update entire layout

   pfocus = focus;                                                               //  remember focus image/text/line
   pfocusii = focusii;

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

   Fupdatebusy = 1;
   start_detached_thread(Lupdate_thread,0);                                      //  trigger image update thread
   while (Fupdatebusy) zsleep(0.01);

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


void * mashup::Lupdate_thread(void *)                                            //  main thread
{
   using namespace mashup;

   if (! Lpxb || ! Fpxb) {
      Fupdatebusy = 0;
      pthread_exit(0);
   }

   do_wthreads(Lupdate_wthread,NWT);                                             //  worker threads

   Fupdatebusy = 0;
   pthread_exit(0);
}


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

   int      index = *((int *) (arg));
   int      ii, jj, 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;
   uint8    *vtranmap;
   PXB      *pxb2;
   float    e3part;

   for (py1 = updylo+index; py1 < updyhi; py1 += NWT)                            //  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];
      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;                                                    //  image size at scale
         hh2 = image[ii].hh2;
         if (image[ii].theta == 0) {
            px2 = px1 - px0;                                                     //  overlay image pixel for layout pixel
            py2 = py1 - py0;
         }
         else {
            sinT = image[ii].sinT;
            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 this image
         if (py2 < 0 || py2 > hh2-1) continue;

         pxb2 = image[ii].pxb2;                                                  //  overlay image virt. pixel
         if (! vpixel(pxb2,px2,py2,vpix2)) continue;                             //  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

         vtranmap = image[ii].vtranmap;
         if (vtranmap)                                                           //  var. transparency (px2,py2)
         {
            ww1 = image[ii].ww1;                                                 //  image size at 1x scale
            hh1 = image[ii].hh1;
            qx = px2 / image[ii].scale;                                          //  position in 1x image
            qy = py2 / image[ii].scale;
            if (qx < ww1 && qy < hh1) {                                          //  position is within image
               jj = qy * ww1 + qx;
               Vtran = vtranmap[jj] / 255.0;                                     //  var. transparency 0...1
               Iopac = Iopac * (1 - Vtran);
            }
         }

         scale = image[ii].scale;

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

   pthread_exit(0);                                                              //  exit thread
}


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

   MONTAGE

   Make an image of selected images, arranged in a compact matrix format.

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

namespace montage_names
{
   int         Fww;                       //  frame image width (input)
   int         Fhh;                       //  frame image height (calculated)
   int         Fmarg;                     //  frame margin size (input)
   int         Ncols;                     //  image columns in frame (input)
   int         Nrows;                     //  image rows in frame (calculated)
   int         Imarg;                     //  image margin size (input)
   int         Iww;                       //  image width (calculated, fixed)
   
   typedef struct {
      int      random;                    //  random number for sorting
      int      Ihh;                       //  image height (calculated, variable)
      int      Ipx, Ipy;                  //  image position in frame image
      int      row, col;                  //  image row and column number
      char     *file;                     //  image filename
      PIXBUF   *pixbuf;                   //  scaled image pixbuf
   }  image_t;

   #define maxNm 1000                     //  max. images in montage
   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

   char        uniquename[100];           //  unique montage file name
   char        montagepath[500];          //  suggested folder to save file
}


//  menu function

void m_montage(GtkWidget *, const char *)
{
   using namespace montage_names;

   int  montage_dialog_event(zdialog* zd, const char *event);
   int  montage_mapimages(void);
   int  montage_showframe(void);
   int  montage_sort(void);
   int  montage_shuffle(void);
   int  montage_spread(void);
   int  montage_mapfile(void);

   PIXBUF   *pixbuf, *pixbuf2;
   GError   *gerror = 0;
   int      ii, nn, err, zstat, ww, hh, makemap;
   int      coldiff, coldiffB, coldiff2;
   image_t  imageB[maxNm];
   char     text[100], *filename, *pp, *pp1, *pp2;
   float    R;

   F1_help_topic = "montage";

   if (FGWM != 'F' && FGWM != 'G') return;                                       //  19.0
   if (checkpend("all")) return;
   Fblock = 1;
   Nimages = 0;

/***
       ______________________________________
      |            Image Montage             |
      |                                      |
      |  [Select Files]  NN files selected   |
      |  Frame Width [___]  Margin [___]     |
      |  Image Columns [___]  Margin [___]   |
      |                                      |
      |                   [Proceed] [Cancel] |
      |______________________________________|

***/

   zdialog *zd = zdialog_new(E2X("Image Montage"),Mwin,Bproceed,Bcancel,null);

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

   //  Frame Width [___]  Margin [___] 
   zdialog_add_widget(zd,"hbox","hbf","dialog");
   zdialog_add_widget(zd,"label","labfw","hbf",E2X("Frame Width"),"space=3");
   zdialog_add_widget(zd,"zspin","Fww","hbf","1000|30000|1|2000","space=3");
   zdialog_add_widget(zd,"label","space","hbf",0,"space=5");
   zdialog_add_widget(zd,"label","labfm","hbf",E2X("Margin"),"space=3");
   zdialog_add_widget(zd,"zspin","Fmarg","hbf","0|100|1|30","space=3");

   //  Image Columns [___]  Margin [___]
   zdialog_add_widget(zd,"hbox","hbi","dialog");
   zdialog_add_widget(zd,"label","labir","hbi",E2X("Image Columns"),"space=3");
   zdialog_add_widget(zd,"zspin","Ncols","hbi","1|100|1|4","space=3");           //  max 100 columns
   zdialog_add_widget(zd,"label","space","hbi",0,"space=5");
   zdialog_add_widget(zd,"label","labim","hbi",E2X("Margin"),"space=3");
   zdialog_add_widget(zd,"zspin","Imarg","hbi","0|50|1|20","space=3");

   zdialog_restore_inputs(zd);                                                   //  restore previous inputs
   zdialog_run(zd,montage_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 + (GScount - 1) / Ncols;                                            //  required image rows
   if (Nrows > maxNr) {
      zmessageACK(Mwin,E2X("exceed %d rows"),maxNr);
      goto cleanup;
   }
   
   if (GScount < Ncols) Ncols = GScount;                                         //  reduce if fewer images
   
   Iww = Fww - 2 * Fmarg - (Ncols - 1) * Imarg;                                  //  frame width less margins
   Iww = Iww / Ncols;                                                            //  scaled image width, all images
   
   zmainloop();
   Ffuncbusy = 1;
   Fbusy_goal = GScount;
   Fbusy_done = 0;

   for (nn = ii = 0; ii < GScount; 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;

      gerror = 0;
      pixbuf = gdk_pixbuf_new_from_file(GSfiles[ii],&gerror);                    //  load pixbuf image of file
      if (! pixbuf) {
         zmessageACK(Mwin,"cannot read: %s",GSfiles[ii]);
         Fbusy_done++;
         continue;
      }

      if (gdk_pixbuf_get_has_alpha(pixbuf)) {                                    //  if alpha channel present,
         pixbuf2 = gdk_pixbuf_stripalpha(pixbuf);                                //    remove it
         g_object_unref(pixbuf);
         pixbuf = pixbuf2;
      }
      
      image[nn].file = zstrdup(GSfiles[ii]);                                     //  image file name

      ww = gdk_pixbuf_get_width(pixbuf);                                         //  image dimensions
      hh = gdk_pixbuf_get_height(pixbuf);
      R = 1.0 * Iww / ww;                                                        //  image scale ratio
      image[nn].Ihh = R * hh;                                                    //  scaled image height
      
      image[nn].row = nn / Ncols;                                                //  initial image row
      image[nn].col = nn - Ncols * image[nn].row;                                //  initial image column

      pixbuf2 = gdk_pixbuf_scale_simple(pixbuf,Iww,image[nn].Ihh,BILINEAR);
      image[nn].pixbuf = pixbuf2;
      g_object_unref(pixbuf);
      
      nn++;                                                                      //  accumulate image count
      Fbusy_done++;
   }
   
   Ffuncbusy = 0;
   Fbusy_goal = 0;

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

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

   coldiff = montage_mapimages();                                                //  map scaled images into frame image
   err = montage_showframe();                                                    //  show completed frame 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] [Cancel] |
         |___________________________________|

***/

   zd = zdialog_new(E2X("Optimize"),Mwin,Bstart,Bstop,Bcancel,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

   while (true)                                                                  //  optimization loop
   {
      snprintf(text,100,E2X("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) goto optdone;                                 //  [x] or [done] or [cancel]
      
      Ffuncbusy = 1;                                                             //  [start] or continue

      while (true)
      {
         coldiff2 = coldiff;                                                     //  shuffle() until no improvement
         montage_shuffle();
         coldiff = montage_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 = montage_showframe();                                              //  show completed frame image
         if (err) goto optdone;
      }
      
      if (coldiff < 3) {                                                         //  stop if column diff. is tiny
         zstat = 2;
         goto optdone;
      }

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

optdone:

   Ffuncbusy = 0;

   if (zstat != 2) goto cleanup;                                                 //  quit if [cancel] or [x]

   zdialog_free(zd);
   zd = 0;

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

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

/***
          ___________________________________
         |   Save with unique montage name   |
         |                                   |
         | unique name: [__________________] |
         |                                   |
         | [x] create image map              |
         |                                   |
         |                   [save] [cancel] |
         |___________________________________|

***/

   zd = zdialog_new(E2X("Save with unique montage name"),Mwin,Bsave,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hbname","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labname","hbname",E2X("unique name:"),"space=5");
   zdialog_add_widget(zd,"zentry","uniquename","hbname","montage","size=30");
   zdialog_add_widget(zd,"check","createmap","dialog",E2X("create image map"),"space=10");
   
   pp = image[0].file;                                                           //  default name: montage-filename     20.0
   pp = strrchr(pp,'/');
   if (pp) snprintf(uniquename,100,"montage-%s",pp+1);
   pp = strrchr(uniquename,'.');                                                 //  remove .ext
   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 montage file name
      if (strlen(uniquename) > 3) break;
      zmessageACK(Mwin,E2X("supply a reasonable name"));
      zd->zstat = 0;
   }

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

   strncatv(montagepath,500,"/",uniquename,null);                                //  .../uniquename
   zdialog_fetch(zd,"createmap",makemap);                                        //  create image map file?
   if (makemap) strncatv(montagepath,500," (fotoxx montage)",null);              //  .../uniquename (fotoxx montage)
   strncatv(montagepath,500,".jpg",null);                                        //  .../uniquename [...].jpg

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

   zdialog_free(zd);
   zd = 0;

   f_open(filename);                                                             //  make it the current file
   
   pp1 = strrchr(filename,'/');                                                  //  get actual file name used
   if (! pp1) goto cleanup;
   pp2 = strstr(pp1," (fotoxx montage)");                                        //  get uniquename, may have changed
   if (! pp2) goto cleanup;
   *pp2 = 0;
   strncpy0(uniquename,pp1+1,100);
   zfree(filename);

   if (makemap) {
      err = montage_mapfile();                                                   //  create corresponding map file
      if (! err) zmessageACK(Mwin,E2X("map file saved: %s"),uniquename);
   }

cleanup:
   if (zd) zdialog_free(zd);                                                     //  kill dialog
   for (ii = 0; ii < Nimages; ii++)
      g_object_unref(image[ii].pixbuf);                                          //  free memory
   Fblock = 0;
   Ffuncbusy = 0;
   gallery(montagepath,"init",0);                                                //  20.0
   gallery(image[0].file,"paint",0);
   m_viewmode(0,"G");
   return;
}


//  montage dialog event and completion function

int montage_dialog_event(zdialog *zd, const char *event)
{
   using namespace montage_names;

   char        countmess[80];

   if (strmatch(event,"files"))                                                  //  select images to process
   {
      zdialog_show(zd,0);
      gallery_select_clear();                                                    //  20.0
      gallery_select();                                                          //  get file list
      snprintf(countmess,80,Bfileselected,GScount);
      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 (GScount == 0) {                                                           //  no files selected
      zmessageACK(Mwin,Bnofileselected);
      return 1;
   }

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

   zdialog_fetch(zd,"Fww",Fww);                                                  //  frame dimensions
   zdialog_fetch(zd,"Fmarg",Fmarg);                                              //  frame margin
   zdialog_fetch(zd,"Ncols",Ncols);                                              //  image columns
   zdialog_fetch(zd,"Imarg",Imarg);                                              //  image margin
   
   zd->zstat = 1;                                                                //  dialog finished
   return 1;
}


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

int montage_mapimages()
{
   using namespace montage_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 = Fmarg + col * (Iww + Imarg);                               //  image x position
      if (row == 0) image[ii].Ipy = Fmarg;                                       //  image y position for row 0
      else image[ii].Ipy = image[jj].Ipy + image[jj].Ihh + Imarg;                //  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 frame image from scaled images and show the frame image

int montage_showframe()
{
   using namespace montage_names;

   uint8    *Fpixels, *pixel;
   int      ii, px, py, Frs, col, maxcol = 0;
   int      ww, hh, stat, bott, err;
   char     *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
   }
   
   Fhh = maxcol + Fmarg;                                                         //  set frame image height
   if (Fhh > wwhh_limit1) {
      zmessageACK(Mwin,E2X("image frame is too large: %d x %d"),Fww,Fhh);
      return 1;
   }

   Fpixbuf = gdk_pixbuf_new(GDKRGB,0,8,Fww,Fhh);                                 //  make frame image pixbuf
   if (! Fpixbuf) {
      zmessageACK(Mwin,E2X("image frame is too large: %d x %d"),Fww,Fhh);
      return 1;                                                                  //  error return
   }

   Fpixels = gdk_pixbuf_get_pixels(Fpixbuf);                                     //  fill frame image with white
   Frs = gdk_pixbuf_get_rowstride(Fpixbuf);
   
   for (py = 0; py < Fhh; py++)
   for (px = 0; px < Fww; px++)
   {
      pixel = Fpixels + py * Frs + px * 3;
      pixel[0] = pixel[1] = pixel[2] = 255;
   }

   for (ii = 0; ii < Nimages; ii++)                                              //  loop all images
   {
      pixbuf = image[ii].pixbuf;                                                 //  copy scaled image into frame 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,30);                                               //  save frame image to temp file
   strcat(file,"/image (fotoxx montage).jpg");
   stat = gdk_pixbuf_save(Fpixbuf,file,"jpeg",&gerror,"quality","90",null);
   if (! stat) {
      zmessageACK(Mwin,"GDK pixbuf: cannot save file");
      zfree(file);
      g_object_unref(Fpixbuf);
      return 2;                                                                  //  error return
   }
   
   m_viewmode(0,"F");
   err = f_open(file);                                                           //  show frame image
   if (err) return 3;

   zfree(file);
   g_object_unref(Fpixbuf);

   return 0;
}


//  shuffle image column assignments to make column heights even

int montage_shuffle()
{
   using namespace montage_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 montage_sort()
{
   using namespace montage_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((char *) 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 montage_spread()
{
   using namespace montage_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 montage file 
//    to the corresponding image files

int montage_mapfile()
{
   using namespace montage_names;

   int      ii, lox, hix, loy, hiy;
   float    flox, fhix, floy, fhiy;
   char     montagemapfile[300];
   FILE     *fid;
   
   snprintf(montagemapfile,300,"%s/%s",montage_maps_folder,uniquename);
   
   fid = fopen(montagemapfile,"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 / Fww;                                                    //  rescale to 0.0 - 1.0
      floy = 1.0 * loy / Fhh;                                                    //  (immune to image resize)
      fhix = 1.0 * hix / Fww;
      fhiy = 1.0 * hiy / Fhh;
      
      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 montage file

void montage_Lclick_func(int mousex, int mousey)
{
   using namespace montage_names;
   
   typedef struct {
      char     *file;                                                            //  image filename
      int      lox, loy;                                                         //  scaled image area in montage image
      int      hix, hiy;
   }  image_t;
   
   image_t     image[maxNm];                                                     //  image data

   int         fww, fhh, Nimages = 0;
   char        buff[500], montagemapfile[300];
   char        *pp, *pp1, *pp2, *file;
   int         ii, cc, nn, err;
   float       flox, fhix, floy, fhiy;
   FILE        *fid;
   STATB       statb;
   
   fww = Fpxb->ww;                                                               //  montage image dimensions
   fhh = Fpxb->hh;   

   pp1 = strrchr(curr_file,'/');                                                 //  extract montage unique name
   if (! pp1) goto notfound;
   pp1++;
   pp2 = strstr(pp1," (fotoxx montage)");
   if (! pp2) goto notfound;
   cc = pp2 - pp1;
   if (cc > 100) goto notfound;
   strncpy0(uniquename,pp1,100);
   uniquename[cc] = 0;

   snprintf(montagemapfile,300,"%s/%s",montage_maps_folder,uniquename);          //  corresp. montage map file
   
   fid = fopen(montagemapfile,"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[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;
   err = stat(file,&statb);
   if (err) {
      zmessageACK(Mwin,strerror(errno));
      goto freemem;
   }
   
   popup_image(file,MWIN,1,512);
   goto freemem;
   
notfound:
   zmessageACK(Mwin,Bfilenotfound2,uniquename);
   goto freemem;

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

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

   return;
}


