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

   Fotoxx      edit photos and manage collections

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

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

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

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

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

   Fotoxx image edit - menu functions for named image collections (albums) 

   m_manage_albums            manage image albums
   album_new                  create a new album
   album_selectfiles          select files, add to cache
   album_cuttocache           remove file position from album, add to cache
   album_pastecache           add files from cache to album at position
   album_pastefile            add file to album at position
   album_remove_cachefiles    remove cached files from album, leave in cache
   album_movefile             move file in album (drag/drop same file)
   album_show                 set and show current album
   file_addtocache            add image file to end of file cache
   clear_filecache            clear image file cache
   load_filecache             load file cache from disk file
   save_filecache             save file cache to disk file
   album_clean                purge missing files from album and notify user
   conv_albums                convert albums when files are moved
   m_copyto_cache             popup menu - add clicked or current file to cache
   m_album_removefile         popup menu - remove clicked file from album
   m_album_cutfile            popup menu - remove file and add to cache
   m_album_pastecache         popup menu - paste file cache at clicked position

   m_slideshow                display album images in sequence with artsy transitions
   m_setdesktop               set desktop wallpaper image from Fotoxx current image
   m_cycledesktop             cycle desktop wallpaper images from a Fotoxx album
   run_cycledesktop           cycle desktop wallpaper execution function

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

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

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

//  Manage Albums - create, view, edit named albums of images

namespace albums
{
   char        *current_album = 0;                                               //  album file name
   char        *filez;
   zdialog     *zdmanagealbums = 0;
   GdkDisplay  *display;
   PIXBUF      *pixbuf;
   GdkCursor   *cursor;
   GdkWindow   *Ggdkwin;
   int         dragNth;
   char        albums_copy[200];
   char        **filecache = 0;                                                  //  cache for moving image files around
   int         Nfilecache = 0;                                                   //  count of image files in cache
   char        cachebuff[XFCC];                                                  //  disk cache IO buffer
};

void album_new();                                                                //  start a new album
void album_selectfiles();                                                        //  select files, add to image cache
void album_cuttocache(int posn, int addcache);                                   //  remove from album, add to image cache
void album_pastecache(int posn, int clear);                                      //  paste image cache into current album
void album_remove_cachefiles();                                                  //  remove cached files from album        16.08
int  file_addtocache(char *file);                                                //  add image file to file cache
void clear_filecache();                                                          //  clear image file cache
void load_filecache();                                                           //  load image file cache from disk
void save_filecache();                                                           //  save image file cache to disk
void album_clean();                                                              //  purge missing files and notify user



void m_manage_albums(GtkWidget *, cchar *)
{
   using namespace albums;

   int      manage_albums_dialog_event(zdialog *zd, cchar *event);               //  manage albumd dialog event func

   cchar    *helptext1 = ZTX("Right-click album thumbnail to cut/copy \n"
                             "to cache, insert from cache, or remove.");
   cchar    *helptext2 = ZTX("Drag album thumbnail to new position.");

   F1_help_topic = "manage_albums";
   if (zdmanagealbums) return;                                                   //  already active
   if (checkpend("all")) return;                                                 //  check nothing pending
   Fblock = 1;

/***
       ____________________________________________
      |                                            |
      |        Manage Albums                       |
      |                                            |
      |  [ New  ]  Create or replace an album      |
      |  [Choose]  Album to view or edit           |
      |  [ Add  ]  Select images to add            |
      |  [Remove]  Select images to remove         |
      |  [Clear ]  Clear image cache               |
      |  [Delete]  Delete an album                 |
      |                                            |
      |  Image cache has NN images                 |
      |                                            |
      |  Right-click album thumbnail to cut/copy   |
      |  to cache, insert from cache, or remove.   |
      |  Drag album thumbnail to new position.     |
      |                                            |
      |                                   [done]   |
      |____________________________________________|

***/

   zdialog *zd = zdialog_new(ZTX("Manage Albums"),Mwin,Bdone,null);
   zdmanagealbums = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=3");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"button","new","vb1",Bnew);
   zdialog_add_widget(zd,"button","choose","vb1",Bchoose);
   zdialog_add_widget(zd,"button","add","vb1",Badd);
   zdialog_add_widget(zd,"button","remove","vb1",Bremove);
   zdialog_add_widget(zd,"button","clear","vb1",Bclear);
   zdialog_add_widget(zd,"button","delete","vb1",Bdelete);
   zdialog_add_widget(zd,"hbox","hbnew","vb2");
   zdialog_add_widget(zd,"label","labnew","hbnew",ZTX("Create or replace an album"));
   zdialog_add_widget(zd,"hbox","hbchoose","vb2");
   zdialog_add_widget(zd,"label","labchoose","hbchoose",ZTX("Album to view or edit"));
   zdialog_add_widget(zd,"hbox","hbadd","vb2");
   zdialog_add_widget(zd,"label","labadd","hbadd",ZTX("Select images to add"));
   zdialog_add_widget(zd,"hbox","hbremv","vb2");
   zdialog_add_widget(zd,"label","labremv","hbremv",ZTX("Select images to remove"));
   zdialog_add_widget(zd,"hbox","hbclear","vb2");
   zdialog_add_widget(zd,"label","labclear","hbclear",ZTX("Clear image cache"));
   zdialog_add_widget(zd,"hbox","hbdelete","vb2");
   zdialog_add_widget(zd,"label","labdelete","hbdelete",ZTX("Delete an album"));
   zdialog_add_widget(zd,"hbox","hbNcache","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labNcache","hbNcache",0,"space=5");
   zdialog_add_widget(zd,"hbox","hbhelp1","dialog",0,"space=8");
   zdialog_add_widget(zd,"label","labhelp1","hbhelp1",helptext1,"space=5");
   zdialog_add_widget(zd,"hbox","hbhelp2","dialog");
   zdialog_add_widget(zd,"label","labhelp2","hbhelp2",helptext2,"space=5");

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


//  manage albums dialog event and completion function

int manage_albums_dialog_event(zdialog *zd, cchar *event)
{
   using namespace albums;

   cchar       *choosealbum = ZTX("Choose Album");
   cchar       *delalbum = ZTX("Delete an album");
   cchar       *ncacheFormat = ZTX("Image cache has %d images");
   cchar       *dumpmess = ZTX("cache added to empty album");
   char        *cfile, ncachetext[60];
   
   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  [done]
   if (strmatch(event,"escape")) zd->zstat = 1;                                  //  escape = done                      15.07

   if (zd->zstat) {                                                              //  [done] or [x]
      zdialog_free(zd);
      zdmanagealbums = 0;
      Fblock = 0;
      return 1;
   }

   if (strmatch(event,"new"))                                                    //  start a new album for editing
   {
      zdialog_show(zd,0);                                                        //  hide manage albums dialog
      album_new();                                                               //  select files, add to image cache
      zdialog_show(zd,1);                                                        //  restore manage albums dialog
   }

   if (strmatch(event,"choose"))                                                 //  choose an album to view or edit
   {
      cfile = zgetfile(choosealbum,MWIN,"file",albums_dirk);                     //  choose album file
      if (! cfile) return 1;
      if (current_album) zfree(current_album);                                   //  set current album for editing
      current_album = cfile;
      album_show();                                                              //  show album
   }

   if (strmatch(event,"add"))                                                    //  select images and add to cache
   {
      zdialog_show(zd,0);                                                        //  hide manage albums dialog

      album_selectfiles();                                                       //  select files, add to image cache

      if (current_album) {
         album_show();                                                           //  restore album gallery
         if (navi::Nfiles == 0) {                                                //  album is empty
            album_pastecache(0,1);                                               //  insert cache into album
            album_show();
            zmessageACK(Mwin,dumpmess);
         }
      }

      zdialog_show(zd,1);                                                        //  restore manage albums dialog
   }

   if (strmatch(event,"remove"))                                                 //  select images to remove            16.08
   {
      zdialog_show(zd,0);                                                        //  hide manage albums dialog

      album_selectfiles();                                                       //  select files, add to image cache

      if (current_album) {
         album_show();                                                           //  restore album gallery
         if (zmessageYN(Mwin,ZTX("remove %d album files?"),Nfilecache)) {
            album_remove_cachefiles();                                           //  removed cached files from album
            album_show();
         }
      }

      zdialog_show(zd,1);                                                        //  restore manage albums dialog
   }

   if (strmatch(event,"clear"))                                                  //  clear image cache
      clear_filecache();

   if (strmatch(event,"delete"))                                                 //  choose album to delete
   {
      cfile = zgetfile(delalbum,MWIN,"file",albums_dirk);                        //  choose album file
      if (! cfile) return 1;
      if (! zmessageYN(Mwin,ZTX("delete %s ?"),cfile)) return 1;

      if (navi::galleryname && strmatch(cfile,navi::galleryname))                //  current gallery is deleted
      {
         zfree(navi::galleryname);                                               //  bugfix  15.07
         navi::galleryname = 0;
         navi::gallerytype = NONE;
         m_viewmode(0,"F");
      }

      if (current_album && strmatch(cfile,current_album)) {
         zfree(current_album);
         current_album = 0;
      }

      remove(cfile);
      zfree(cfile);
   }

   load_filecache();                                                             //  get image file cache from disk     16.07
   snprintf(ncachetext,60,ncacheFormat,Nfilecache);                              //  update cache count                 15.11
   zdialog_stuff(zd,"labNcache",ncachetext);

   return 1;
}


//  create a new album

void album_new()
{
   using namespace albums;

   int album_new_dialog_event(zdialog *zd, cchar *event);
   char  cachelab[100];

   load_filecache();                                                             //  load image file cache from disk    16.07
   snprintf(cachelab,100,ZTX("fill from image cache (%d images)"),Nfilecache);

/***
       __________________________________________________
      |         Create or replace an album               |
      |                                                  |
      |  Album Name [____________________] [Browse]      |
      |     (o)  make an initially empty album           |
      |     (o)  fill from the image cache (N images)    |
      |     (o)  fill from the current gallery           |
      |                                                  |
      |                                 [done] [cancel]  |
      |__________________________________________________|

***/

   zdialog *zd = zdialog_new(ZTX("Create or replace an album"),Mwin,Bdone,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hbname","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labname","hbname",ZTX("Album Name"),"space=3");
   zdialog_add_widget(zd,"entry","albumname","hbname",0,"space=3|size=20");
   zdialog_add_widget(zd,"button","browse","hbname",Bbrowse,"space=3");
   zdialog_add_widget(zd,"hbox","hbopt","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","space","hbopt",0,"space=10");
   zdialog_add_widget(zd,"vbox","vbopt","hbopt");
   zdialog_add_widget(zd,"radio","empty","vbopt",ZTX("make an initially empty album"));
   zdialog_add_widget(zd,"radio","cache","vbopt",cachelab);
   zdialog_add_widget(zd,"radio","gallery","vbopt",ZTX("fill from current gallery"));

   zdialog_stuff(zd,"empty",1);
   zdialog_stuff(zd,"cache",0);
   zdialog_stuff(zd,"gallery",0);

   zdialog_run(zd,album_new_dialog_event);
   zdialog_wait(zd);
   zdialog_free(zd);
   return;
}


//  dialog event and completion function

int album_new_dialog_event(zdialog *zd, cchar *event)
{
   using namespace albums;

   int         Fempty, Fcache, Fgallery;
   char        *cfile, *pp, albumname[100], albumfile[200];
   int         Nth;
   FTYPE       ftype;
   FILE        *fid;

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

   if (strmatch(event,"browse"))
   {
      cfile = zgetfile(ZTX("Album Name"),MWIN,"save",albums_dirk);               //  choose file name
      if (! cfile) return 1;
      pp = strrchr(cfile,'/');
      if (! pp) return 1;
      zdialog_stuff(zd,"albumname",pp+1);
      zfree(cfile);
      return 1;
   }

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

   zdialog_fetch(zd,"albumname",albumname,100);                                  //  get album name
   if (*albumname <= ' ') {
      zmessageACK(Mwin,ZTX("enter an album name"));
      zd->zstat = 0;                                                             //  keep dialog active
      return 1;
   }

   snprintf(albumfile,200,"%s/%s",albums_dirk,albumname);                        //  make filespec
   
   fid = fopen(albumfile,"w");                                                   //  open/write empty album file
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return 1;
   }

   fclose(fid);

   if (current_album) zfree(current_album);                                      //  set current album for editing
   current_album = zstrdup(albumfile);

   zdialog_fetch(zd,"empty",Fempty);                                             //  get option
   zdialog_fetch(zd,"cache",Fcache);
   zdialog_fetch(zd,"gallery",Fgallery);

   if (Fcache) album_pastecache(0,1);                                            //  fill album from image cache

   if (Fgallery)                                                                 //  fill album from gallery
   {
      if (navi::Nimages == 0) {
         zmessageACK(Mwin,ZTX("gallery is empty"));
         return 1;
      }

      fid = fopen(current_album,"w");                                            //  open/write album file
      if (! fid) {
         zmessageACK(Mwin,strerror(errno));
         return 1;
      }

      for (Nth = 0; Nth < navi::Nfiles; Nth++)                                   //  add gallery images to album file
      {
         pp = gallery(0,"find",Nth);
         if (! pp) break;
         ftype = image_file_type(pp);                                            //  must be image or RAW file
         if (ftype != IMAGE && ftype != RAW) {
            zfree(pp);
            continue;
         }
         fprintf(fid,"%s\n",pp);
         zfree(pp);
      }

      fclose(fid);
   }

   album_show();
   zmessage_post(Mwin,3,ZTX("new album created"));
   return 1;
}


//  select files from gallery thumbnails, add to image cache

void album_selectfiles()
{
   using namespace albums;

   int      ii;
   char     **selfiles = 0;

   selfiles = gallery_getfiles(filecache);                                       //  get cached files + new files if any
   if (! selfiles) return;                                                       //  nothing selected
   
   clear_filecache();                                                            //  replace file cache
   filecache = selfiles;
   
   for (ii = 0; selfiles[ii]; ii++);                                             //  count entries
   Nfilecache = ii;
   
   save_filecache();                                                             //  update file cache file             16.07
   return;
}


//  remove file at position from album, optionally add to image cache

void album_cuttocache(int posn, int addcache)
{
   using namespace albums;

   int         ii;
   char        *pp;
   FILE        *fidr, *fidw;

   if (! current_album) return;

   snprintf(albums_copy,199,"%s/albums_copy",get_zuserdir());                    //  albums copy file

   fidw = fopen(albums_copy,"w");                                                //  open/write copy file
   if (! fidw) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   fidr = fopen(current_album,"r");                                              //  open/read album file
   if (! fidr) {
      zmessageACK(Mwin,strerror(errno));
      fclose(fidw);
      return;
   }

   for (ii = 0; ; ii++)                                                          //  loop album member files
   {
      pp = fgets_trim(cachebuff,XFCC,fidr);                                      //  album member file
      if (! pp) break;

      if (ii == posn) {                                                          //  position to remove
         if (addcache) file_addtocache(pp);                                      //  add to image cache if wanted
         continue;                                                               //  omit from copy file
      }

      fprintf(fidw,"%s\n",pp);                                                   //  add to copy file
   }

   fclose(fidr);
   fclose(fidw);

   rename(albums_copy,current_album);                                            //  replace album file with copy

   return;
}


//  insert image cache files into album at designated position

void album_pastecache(int posn, int clear)
{
   using namespace albums;

   int      ii, jj;
   char     *pp;
   FILE     *fidr, *fidw;

   if (! current_album) return;
   
   load_filecache();                                                             //  load image cache from disk         16.07
   if (Nfilecache == 0) return;

   snprintf(albums_copy,199,"%s/albums_copy",get_zuserdir());                    //  albums copy file

   fidw = fopen(albums_copy,"w");                                                //  open/write copy file
   if (! fidw) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   fidr = fopen(current_album,"r");                                              //  open/read album file
   if (! fidr) {
      zmessageACK(Mwin,strerror(errno));
      fclose(fidw);
      return;
   }

   for (ii = 0; ; ii++)                                                          //  loop album member files
   {
      if (ii == posn)
         for (jj = 0; jj < Nfilecache; jj++)                                     //  add cached files here
            fprintf(fidw,"%s\n",filecache[jj]);

      pp = fgets_trim(cachebuff,XFCC,fidr);                                      //  copy album member file
      if (! pp) break;                                                           //  EOF
      fprintf(fidw,"%s\n",pp);
   }

   fclose(fidr);
   fclose(fidw);

   rename(albums_copy,current_album);                                            //  replace album file with copy
   if (clear) clear_filecache();
   return;
}


//  insert image file into album at designated position

void album_pastefile(char *file, int posn)
{
   using namespace albums;

   int      ii;
   char     *pp;
   FILE     *fidr, *fidw;

   if (! current_album) return;
   if (! file) return;

   snprintf(albums_copy,199,"%s/albums_copy",get_zuserdir());                    //  albums copy file

   fidw = fopen(albums_copy,"w");                                                //  open/write copy file
   if (! fidw) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   fidr = fopen(current_album,"r");                                              //  open/read album file
   if (! fidr) {
      zmessageACK(Mwin,strerror(errno));
      fclose(fidw);
      return;
   }

   for (ii = 0; ; ii++)                                                          //  loop album member files
   {
      if (ii == posn) fprintf(fidw,"%s\n",file);                                 //  insert current file here
      pp = fgets_trim(cachebuff,XFCC,fidr);                                      //  copy album member file
      if (! pp) break;                                                           //  EOF
      fprintf(fidw,"%s\n",pp);
   }

   fclose(fidr);
   fclose(fidw);

   rename(albums_copy,current_album);                                            //  replace album file with copy
   return;
}


//  Remove all files in cache from album - leave them in cache.
//  If file is present multiple times in album, all will be deleted.

void album_remove_cachefiles()                                                   //  16.08
{
   using namespace albums;

   int         ii, jj;
   char        *pp;
   FILE        *fidr, *fidw;

   if (! current_album) return;

   snprintf(albums_copy,199,"%s/albums_copy",get_zuserdir());                    //  albums copy file

   fidw = fopen(albums_copy,"w");                                                //  open/write copy file
   if (! fidw) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   fidr = fopen(current_album,"r");                                              //  open/read album file
   if (! fidr) {
      zmessageACK(Mwin,strerror(errno));
      fclose(fidw);
      return;
   }

   for (ii = 0; ; ii++)                                                          //  loop album member files
   {
      pp = fgets_trim(cachebuff,XFCC,fidr);                                      //  album member file
      if (! pp) break;
      
      for (jj = 0; jj < Nfilecache; jj++)                                        //  compare to all cached files
         if (strmatch(pp,filecache[jj])) break;
      if (jj < Nfilecache) continue;                                             //  if match found, omit file

      fprintf(fidw,"%s\n",pp);                                                   //  add to copy file
   }

   fclose(fidr);
   fclose(fidw);

   rename(albums_copy,current_album);                                            //  replace album file with copy

   return;
}


//  move an album file from pos1 to pos2 (for drag and drop)

void album_movefile(int pos1, int pos2)                                          //  16.08
{
   using namespace albums;

   int         ii;
   char        *pp;
   FILE        *fidr, *fidw;

   if (! current_album) return;
   if (pos1 == pos2) return;

   filez = gallery(0,"find",pos1);
   if (! filez) return;

   snprintf(albums_copy,199,"%s/albums_copy",get_zuserdir());                    //  albums copy file

   fidw = fopen(albums_copy,"w");                                                //  open/write copy file
   if (! fidw) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   fidr = fopen(current_album,"r");                                              //  open/read album file
   if (! fidr) {
      zmessageACK(Mwin,strerror(errno));
      fclose(fidw);
      return;
   }

   for (ii = 0; ; ii++)                                                          //  loop album member files
   {
      pp = fgets_trim(cachebuff,XFCC,fidr);                                      //  album member file
      if (! pp) break;

      if (ii == pos1) continue;                                                  //  skip file position to move from

      if (ii == pos2) {                                                          //  file position to move to
         fprintf(fidw,"%s\n",filez);                                             //  insert the moved file here
         pos2 = -1;
      }

      fprintf(fidw,"%s\n",pp);                                                   //  copy to output file
   }

   if (pos2 >= 0) fprintf(fidw,"%s\n",filez);                                    //  add at the end

   fclose(fidr);
   fclose(fidw);

   rename(albums_copy,current_album);                                            //  replace album file with copy
   return;
}


//  show the current album gallery

void album_show(char *file) 
{
   using namespace albums;
   
   if (file) {                                                                   //  16.04
      if (current_album) zfree(current_album);
      current_album = zstrdup(file);
   }

   if (! current_album) return;
   album_clean();                                                                //  purge missing files, notify user   16.04
   free_resources();                                                             //  no current file
   navi::gallerytype = ALBUM;
   gallery(current_album,"initF");
   m_viewmode(0,"G");
   gallery(0,"paint",-1);
   return;
}


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

//  add image file to image cache at end position
//  returns current cache count

int file_addtocache(char *file)
{
   using namespace albums;

   int      cc, cc1, cc2;
   char     **newcache;

   load_filecache();                                                             //  load image file cache from disk    16.07

   cc = sizeof(char *);
   cc1 = Nfilecache * cc;                                                        //  current cache size
   cc2 = cc1 + cc;                                                               //  new cache size, 1 more
   newcache = (char **) zmalloc(cc2);

   if (cc1) {
      memcpy(newcache,filecache,cc1);                                            //  copy old cache to new cache
      zfree(filecache);                                                          //    and free old cache
   }

   filecache = newcache;                                                         //  new bigger cache
   filecache[Nfilecache] = zstrdup(file);                                        //  add new image file at the end
   Nfilecache++;

   save_filecache();                                                             //  update imae file cache file        16.07
   return Nfilecache;
}


//  clear the image file cache

void clear_filecache()                                                           //  16.07
{
   using namespace albums;

   if (! Nfilecache) return;
   for (int ii = 0; ii < Nfilecache; ii++)                                       //  bugfix                             16.05
      zfree(filecache[ii]);
   zfree(filecache);
   filecache = 0;
   Nfilecache = 0;
   save_filecache();                                                             //  update imae file cache file        16.07
   return;
}


//  load image file cache from disk

void load_filecache()                                                            //  16.07
{
   using namespace albums;

   FILE     *fid;
   char     *pp;
   int      ii, cc;
   
   if (Nfilecache) {
      for (int ii = 0; ii < Nfilecache; ii++)                                    //  clear memory file cache
         zfree(filecache[ii]);
      zfree(filecache);
   }
   filecache = 0;
   Nfilecache = 0;

   fid = fopen(filecachefile,"r");                                               //  open image cache file
   if (! fid) return;
   
   ii = 0;
   while ((pp = fgets_trim(cachebuff,XFCC,fid))) ii++;                           //  count files in image cache
   fclose(fid);
   if (! ii) return;
   
   cc = ii * sizeof(char *);                                                     //  allocate memory
   filecache = (char **) zmalloc(cc);

   fid = fopen(filecachefile,"r");                                               //  open image cache file
   if (! fid) zappcrash(strerror(errno));

   ii = 0;
   while ((pp = fgets_trim(cachebuff,XFCC,fid))) {                               //  read all recs = image files
      filecache[ii] = zstrdup(pp);                                               //  add to file cache in memory
      ii++;
   }
   fclose(fid);
   Nfilecache = ii;

   if (zdmanagealbums) zdialog_send_event(zdmanagealbums,"focus");               //  update image cache count           16.07
   return;
}


//  save image file cache to disk

void save_filecache()                                                            //  16.07
{
   using namespace albums;

   FILE     *fid;
   int      ii, jj = 0;

   fid = fopen(filecachefile,"w");                                               //  open image cache file
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }
   
   for (ii = 0; ii < Nfilecache; ii++) {
      jj = fprintf(fid,"%s\n",filecache[ii]);                                    //  write to output file
      if (jj < 0) break;
   }
   fclose(fid);
   if (jj < 0) zmessageACK(Mwin,strerror(errno));

   if (zdmanagealbums) zdialog_send_event(zdmanagealbums,"focus");               //  update image cache count           16.07
   return;
}


//  purge missing files from current album and notify user

void album_clean()                                                               //  16.04
{
   using namespace albums;

   GtkWidget   *popwin = 0;
   FTYPE        ftype;
   char        *pp, *albumname, poptitle[100];
   FILE        *fidr, *fidw;

   if (! current_album) return;
   
   fidr = fopen(current_album,"r");                                              //  open/read album file
   if (! fidr) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   while (true)                                                                  //  read album member files
   {
      pp = fgets_trim(cachebuff,XFCC,fidr);
      if (! pp) break;
      ftype = image_file_type(pp);                                               //  look for missing files
      if (ftype == IMAGE || ftype == RAW) continue;
      if (! popwin) {                                                            //  notify user
         albumname = strrchr(current_album,'/');
         if (albumname) albumname++;
         snprintf(poptitle,100,"album %s - missing files",albumname);
         popwin = write_popup_text("open",poptitle,600,300,Mwin);
      }
      write_popup_text("write",pp);
   }

   fclose(fidr);
   
   if (! popwin) return;                                                         //  nothing missing

   snprintf(albums_copy,199,"%s/albums_copy",get_zuserdir());                    //  albums copy file

   fidw = fopen(albums_copy,"w");                                                //  open/write copy file
   if (! fidw) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   fidr = fopen(current_album,"r");                                              //  open/read album file
   if (! fidr) {
      zmessageACK(Mwin,strerror(errno));
      fclose(fidw);
      return;
   }

   while (true)                                                                  //  copy album member files
   {
      pp = fgets_trim(cachebuff,XFCC,fidr);
      if (! pp) break;
      ftype = image_file_type(pp);                                               //  remove deleted files
      if (ftype != IMAGE && ftype != RAW) continue;
      fprintf(fidw,"%s\n",pp);
   }

   fclose(fidr);
   fclose(fidw);
   
   rename(albums_copy,current_album);                                            //  replace album file with copy
   return;
}


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

//  Fix albums when image files have been renamed or moved.
//  inputs: a list of old filespecs and corresponding new filespecs.
//  used by batch_convert().

void conv_albums(char **oldfiles, char **newfiles, int nfiles) 
{
   char        *pp, *albumnames[999];
   char        buffr[XFCC], buffw[XFCC];
   char        albumfile[200], tempfile[200];
   int         ii, jj, err;
   int         Nalbum, contx;
   FILE        *fidr, *fidw;
   cchar       *findcomm = "find -L \"%s\" -type f";

   for (contx = ii = 0; ii < 999; ii++) {
      pp = command_output(contx,findcomm,albums_dirk);                           //  find all album files
      if (! pp) break;
      albumnames[ii] = pp;
   }
   if (contx) command_kill(contx);

   Nalbum = ii;
   if (! Nalbum) return;
   if (Nalbum == 999)
      zmessageACK(Mwin,"999 albums limit reached");

   fidr = fidw = 0;
   *tempfile = 0;

   for (ii = 0; ii < Nalbum; ii++)                                               //  loop all albums
   {
      strcpy(albumfile,albumnames[ii]);                                          //  album file - input
      strcpy(tempfile,albumfile);                                                //  temp file - output
      strcat(tempfile,".new");

      fidr = fopen(albumfile,"r");
      if (! fidr) goto error;
      fidw = fopen(tempfile,"w");
      if (! fidw) goto error;

      while ((pp = fgets_trim(buffr,XFCC,fidr)))                                 //  read all album recs = image files
      {
         for (jj = 0; jj < nfiles; jj++)                                         //  list of renamed/moved files
            if (strmatch(buffr,oldfiles[jj])) break;                             //    includes this file?

         if (jj == nfiles) strcpy(buffw,buffr);                                  //  no, copy old filespec
         else strcpy(buffw,newfiles[jj]);                                        //  yes, copy corresp. new filespec

         jj = fprintf(fidw,"%s\n",buffw);                                        //  write to output file
         if (jj < 0) goto error;
      }

      err = fclose(fidr);
      if (err) goto error;
      err = fclose(fidw);
      if (err) goto error;
      fidr = fidw = 0;
      err = rename(tempfile,albumfile);                                          //  replace album with temp file
      *tempfile = 0;
      if (err) goto error;
   }

   goto cleanup;

error:
   zmessageACK(Mwin,"%s \n %s",albumfile,strerror(errno));

cleanup:
   if (fidr) fclose(fidr);
   if (fidw) fclose(fidw);
   if (*tempfile) remove(tempfile);
   *tempfile = 0;
   for (ii = 0; ii < Nalbum; ii++)
      zfree(albumnames[ii]);
   Nalbum = 0;
   return;
}


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

//  popup menu - add clicked file or current file to file cache

void m_copyto_cache(GtkWidget *, cchar *)
{
   if (clicked_file) {
      file_addtocache(clicked_file);
      zfree(clicked_file);
      clicked_file = 0;
   }
   else if (curr_file)
      file_addtocache(curr_file);

   return;
}


//  popup menu - remove clicked file from album

void m_album_removefile(GtkWidget *, cchar *menu)
{
   using namespace albums;

   if (! clicked_file) return;

   if (navi::gallerytype != ALBUM) {
      zfree(clicked_file);                                                       //  reset clicked file
      clicked_file = 0;
      return;
   }

   if (current_album) zfree(current_album);                                      //  album being edited
   current_album = zstrdup(navi::galleryname);

   album_cuttocache(clicked_posn,0);                                             //  remove clicked file

   zfree(clicked_file);                                                          //  reset clicked file
   clicked_file = 0;

   album_show();
   return;
}


//  popup menu - remove clicked file from album, add to cache

void m_album_cutfile(GtkWidget *, cchar *menu)
{
   using namespace albums;

   if (! clicked_file) return;

   if (navi::gallerytype != ALBUM) {
      zfree(clicked_file);                                                       //  reset clicked file
      clicked_file = 0;
      return;
   }

   if (current_album) zfree(current_album);                                      //  album being edited
   current_album = zstrdup(navi::galleryname);

   album_cuttocache(clicked_posn,1);                                             //  remove clicked file, add to cache

   zfree(clicked_file);                                                          //  reset clicked file
   clicked_file = 0;

   album_show();                                                                 //  show revised album
   return;
}


//  popup menu - paste image cache at clicked position
//  optionally clear the cache

void m_album_pastecache(GtkWidget *, cchar *menu)
{
   using namespace albums;

   int      posn, clear;

   if (! clicked_file) return;

   if (navi::gallerytype != ALBUM) {                                             //  clicked file not an album
      zfree(clicked_file);                                                       //  reset clicked file
      clicked_file = 0;
      return;
   }

   if (current_album) zfree(current_album);                                      //  album being edited
   current_album = zstrdup(navi::galleryname);

   posn = clicked_posn;
   if (clicked_width > 50) ++posn;                                               //  thumbnail right side clicked, bump posn

   if (strmatch(menu,"clear")) clear = 1;                                        //  set clear cache option
   else clear = 0;
   album_pastecache(posn,clear);                                                 //  paste cached images at position

   zfree(clicked_file);                                                          //  reset clicked file
   clicked_file = 0;

   album_show();                                                                 //  show revised album
   return;
}


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

//  slide show function

int   slideshow_dialog_event(zdialog *zd, cchar *event);                         //  user dialogs
void  ss_transprefs_dialog();                                                    //  edit transition preferences
void  ss_imageprefs_dialog();                                                    //  edit image preferences
void  ss_loadprefs();                                                            //  load preferences from file
void  ss_saveprefs();                                                            //  write preferences to file
int   ss_timerfunc(void *);                                                      //  timer function
int   ss_nextrans();                                                             //  select next transition to use
void  ss_blankwindow();                                                          //  blank the window
void  ss_showcapcom(int mode);                                                   //  show captions and comments

PIXBUF *ss_loadpxb(char *file);                                                  //  load image as pixbuf
PIXBUF *ss_zoom_posn(char *file, int mode, float zoom, int fast);                //  position zoomed image in pixbuf

void  ss_instant();                                                              //  transition functions
void  ss_fadein();
void  ss_rollright();
void  ss_rolldown();
void  ss_venetian();
void  ss_grate();
void  ss_rectangle();
void  ss_implode();
void  ss_explode();
void  ss_radar();
void  ss_spiral();
void  ss_japfan();
void  ss_jaws();
void  ss_ellipse();
void  ss_raindrops();
void  ss_doubledoor();
void  ss_rotate();
void  ss_fallover();
void  ss_spheroid();
void  ss_turnpage();
void  ss_frenchdoor();
void  ss_zoomin();
void  ss_zoomout();

char        ss_albumfile[300] = "";                                              //  slide show album file
char        *ss_albumname = 0;                                                   //  album name (ss_albumfile tail)
int         ss_Nfiles = 0;                                                       //  album file count
int         ss_seconds = 0;                                                      //  standard display time from user
int         ss_cliplimit = 10;                                                   //  image clipping limit from user
char        ss_musicfile[500] = "none";                                          //  /directory.../musicfile.ogg
int         ss_random = 0;                                                       //  use random transitions option from user
int         ss_fullscreen = 0;                                                   //  flag, full screen mode
int         ss_replay = 0;                                                       //  flag, start over when done
int         ss_slowdown = 0;                                                     //  transition slowdown factor 0-99
float       ss_zoomsize = 1.0;                                                   //  zoom-in size, 1-3.0 = 3x
int         ss_zoomsteps;                                                        //  zoom-in steps
int         ss_zoomlocx, ss_zoomlocy;                                            //  zoom-in target (50/50 = image midpoint)
int         ss_setzloc;                                                          //  1-shot flag for image prefs dialog
int         ss_ww, ss_hh, ss_rs;                                                 //  slide show image size, rowstride
char        *ss_oldfile, *ss_newfile;                                            //  image files for transition
PIXBUF      *ss_pxbold, *ss_pxbnew;                                              //  pixbuf images: old, new
double      ss_timer = 0;                                                        //  slide show timer
cchar       *ss_state = 0;                                                       //  slide show state
int         ss_Fcurrent = 0;                                                     //  next file to show
int         ss_Flast = -1;                                                       //  last file shown
int         ss_blank = 0;                                                        //  flag, user pressed B key (blank screen)
cairo_t     *ss_cr;                                                              //  cairo context
int         ss_nwt;                                                              //  threads for transition funcs       16.07

#define     SSNF 21                                                              //  slide show transition types
#define     SSMAXI 10000                                                         //  max. no. slide show images

struct ss_trantab_t {                                                            //  transition table
   char     tranname[32];                                                        //  transition name
   int      enabled;                                                             //  enabled or not
   int      slowdown;                                                            //  slowdown factor
   int      preference;                                                          //  relative preference, 0-99
   void     (*func)();                                                           //  function to perform transition
};

ss_trantab_t  ss_trantab[SSNF];                                                  //  specific slide show transition prefs

int      ss_Tused[SSNF];                                                         //  list of transition types enabled
int      ss_Tlast[SSNF];                                                         //  last transitions used, in order
int      ss_Nused;                                                               //  count of enabled transitions, 0-SSNF
int      ss_Tnext;                                                               //  next transition to use >> last one used

ss_trantab_t  ss_trantab_default[SSNF] =                                         //  transition defaults
{    //   name           enab  slow   pref   function                            //  (enabled, slowdown, preference)
      {  "instant",       1,     0,    10,   ss_instant     },
      {  "fade-in",       1,    15,    10,   ss_fadein      },
      {  "roll-right",    1,     7,    10,   ss_rollright   },                   //  NO BLANKS IN TRANSITION NAMES
      {  "roll-down",     1,     7,    10,   ss_rolldown    },
      {  "venetian",      1,     7,    10,   ss_venetian    },                   //  transitions about 2 seconds for
      {  "grate",         1,     5,    10,   ss_grate       },                   //    CPU = 3 GHz x 4 cores
      {  "rectangle",     1,     6,    10,   ss_rectangle   },                   //    window = 1800 x 1200 
      {  "implode",       1,     8,    10,   ss_implode     },
      {  "explode",       1,     5,    10,   ss_explode     },
      {  "radar",         1,     4,    10,   ss_radar       },
      {  "spiral",        1,     3,    10,   ss_spiral      },
      {  "Japanese-fan",  1,    10,    10,   ss_japfan      },
      {  "jaws",          1,     8,    10,   ss_jaws        },
      {  "ellipse",       1,    16,    10,   ss_ellipse     },
      {  "raindrops",     1,     6,    10,   ss_raindrops   },
      {  "doubledoor",    1,    14,    10,   ss_doubledoor  },
      {  "rotate",        1,     5,    10,   ss_rotate      },
      {  "fallover",      1,    10,    10,   ss_fallover    },
      {  "spheroid",      1,    14,    10,   ss_spheroid    },
      {  "turn-page",     1,    18,    10,   ss_turnpage    },
      {  "french-door",   1,     8,    10,   ss_frenchdoor  }
};

struct ss_imagetab_t   {                                                         //  image table
   char     *imagefile;                                                          //  image file
   int      tone;                                                                //  flag, play tone when shown
   int      capsecs, commsecs;                                                   //  seconds to show caption, comments
   int      wait1;                                                               //  seconds to wait
   int      zoomtype;                                                            //  0/1/2 = none/zoomin/zoomout        15.07
   float    zoomsize;                                                            //  zoom-in size, 1-3.0 = 3x
   int      zoomsteps;                                                           //  zoom-in steps
   int      zoomlocx, zoomlocy;                                                  //  zoom-in target (50/50 = image midpoint)
   int      wait2;                                                               //  seconds to wait
   char     tranname[32];                                                        //  transition type to use
};

ss_imagetab_t  ss_imagetab[SSMAXI];                                              //  specific slide show image table


//  menu function - start or stop a slide show

void m_slideshow(GtkWidget *, cchar *)                                           //  overhauled
{
   zdialog     *zd;
   int         zstat, ii;
   char        countmess[50], *pp;

   ZTX("instant");                                                               //  add translations to .po file       15.09
   ZTX("fade-in");
   ZTX("roll-right");
   ZTX("roll-down");
   ZTX("venetian");
   ZTX("grate");
   ZTX("rectangle");
   ZTX("implode");
   ZTX("explode");
   ZTX("radar");
   ZTX("spiral");
   ZTX("Japanese-fan");
   ZTX("jaws");
   ZTX("ellipse");
   ZTX("raindrops");
   ZTX("doubledoor");
   ZTX("rotate");
   ZTX("fallover");
   ZTX("spheroid");
   ZTX("turn-page");
   ZTX("french-door");

   F1_help_topic = "slide_show";
   if (checkpend("all")) return;                                                 //  check nothing pending
   
   ss_nwt = NWT - 1;                                                             //  transition func threads            16.07

/***
       ________________________________________________________
      | [x] [-] [ ]   Slide Show                               |
      |                                                        |
      | [Select] album-name   123 images                       |
      | Seconds [___|-+]    Clip Limit (%) [___|-+]            |
      | Music File: [_______________________________] [Browse] |
      | [x] Full Screen  [x] Auto-replay  [KB functions]       |
      | Customize: [transitions]  [image files]                |
      |                                                        |
      |                                    [Proceed] [Cancel]  |
      |________________________________________________________|

***/

   zd = zdialog_new(ZTX("Slide Show"),Mwin,Bproceed,Bcancel,null);               //  user dialog

   zdialog_add_widget(zd,"hbox","hbss","dialog",0,"space=3");
   zdialog_add_widget(zd,"button","selectalbum","hbss",Bselect,"space=5");
   zdialog_add_widget(zd,"label","albumname","hbss",Bnoselection,"space=5");
   zdialog_add_widget(zd,"label","nfiles","hbss",Bnoimages,"space=5");

   zdialog_add_widget(zd,"hbox","hbprefs","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labsecs","hbprefs",Bseconds,"space=5");
   zdialog_add_widget(zd,"spin","seconds","hbprefs","1|99|1|3");
   zdialog_add_widget(zd,"label","space","hbprefs",0,"space=5");
   zdialog_add_widget(zd,"label","labclip","hbprefs",ZTX("Clip Limit"),"space=5");
   zdialog_add_widget(zd,"spin","cliplim","hbprefs","0|50|1|0");

   zdialog_add_widget(zd,"hbox","hbmuf","dialog",0,"space=2");
   zdialog_add_widget(zd,"label","labmf","hbmuf",ZTX("Music File"),"space=3");
   zdialog_add_widget(zd,"entry","musicfile","hbmuf","none","size=40|space=5");
   zdialog_add_widget(zd,"button","browse","hbmuf",Bbrowse,"space=5");

   zdialog_add_widget(zd,"hbox","hbscreen","dialog",0,"space=2");
   zdialog_add_widget(zd,"check","fullscreen","hbscreen",ZTX("Full Screen"),"space=3");
   zdialog_add_widget(zd,"check","replay","hbscreen",ZTX("Auto-replay"),"space=5");
   zdialog_add_widget(zd,"button","shortcuts","hbscreen",ZTX("KB functions"),"space=5");

   zdialog_add_widget(zd,"hbox","hbcust","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labprefs","hbcust",ZTX("Customize:"),"space=5");
   zdialog_add_widget(zd,"button","transprefs","hbcust",ZTX("transitions"),"space=5");
   zdialog_add_widget(zd,"button","imageprefs","hbcust",ZTX("image files"),"space=5");

   if (ss_Nfiles) {                                                              //  if album file available,
      ss_loadprefs();                                                            //    load slide show data or defaults
      zdialog_stuff(zd,"albumname",ss_albumname);
      snprintf(countmess,50,ZTX("%d images"),ss_Nfiles);
      zdialog_stuff(zd,"nfiles",countmess);
      zdialog_stuff(zd,"seconds",ss_seconds);
      zdialog_stuff(zd,"cliplim",ss_cliplimit);
      zdialog_stuff(zd,"musicfile",ss_musicfile);
      zdialog_stuff(zd,"fullscreen",ss_fullscreen);                              //  15.03
      zdialog_stuff(zd,"replay",ss_replay);
      navi::gallerytype = ALBUM;                                                 //  open gallery with slide show album
      gallery(ss_albumfile,"initF");
      gallery(0,"paint",ss_Fcurrent);
      m_viewmode(0,"G");
   }

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

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

   if (! ss_Nfiles) {                                                            //  no selection
      zmessageACK(Mwin,ZTX("invalid album"));
      zdialog_free(zd);
      return;
   }

   zdialog_fetch(zd,"seconds",ss_seconds);                                       //  timer interval, seconds
   if (ss_Nused == 0) ss_seconds = 9999;                                         //  if only arrow-keys used, huge interval
   zdialog_fetch(zd,"cliplim",ss_cliplimit);                                     //  image clipping limit
   zdialog_fetch(zd,"musicfile",ss_musicfile,500);                               //  music file
   zdialog_fetch(zd,"fullscreen",ss_fullscreen);                                 //  full screen option
   zdialog_fetch(zd,"replay",ss_replay);                                         //  replay option (last --> first image)

   zdialog_free(zd);                                                             //  kill dialog

   ss_saveprefs();                                                               //  save preference changes

   if (curr_file) {                                                              //  start at curr. file
      for (ii = 0; ii < ss_Nfiles; ii++)                                         //    if member of file list
         if (strmatch(curr_file,ss_imagetab[ii].imagefile)) break;
      if (ii == ss_Nfiles) ii = 0;
   }
   else ii = 0;                                                                  //  else first image                   16.06
   ss_Fcurrent = ii;                                                             //  next file in list to show

   f_open(ss_imagetab[ii].imagefile);                                            //  15.11

   m_viewmode(0,"F");                                                            //  insure tab F

   ss_newfile = 0;                                                               //  no new image
   ss_pxbnew = 0;
   ss_oldfile = 0;                                                               //  no old (prior) image
   ss_pxbold = 0;

   Fslideshow = 1;                                                               //  slideshow active for KB events
   ss_blank = 0;                                                                 //  not blank window
   ss_state = "first";
   Fblowup = 1;                                                                  //  expand small images
   Fzoom = 0;                                                                    //  fit window

   pp = ss_musicfile;                                                            //  start music if any
   if (*pp == '/') shell_ack("paplay \"%s\" &",pp);

   if (ss_fullscreen) {
      win_fullscreen(1);                                                         //  full screen, hide menu and panel
      ss_ww = screenww;                                                          //  drawing window size                16.02
      ss_hh = screenhh;
   }
   else {
      zmainloop();                                                               //  GTK bug                            16.04
      Dww = gdk_window_get_width(gdkwin);                                        //  window size   bugfix               16.02.1
      Dhh = gdk_window_get_height(gdkwin);
      ss_ww = Dww;                                                               //  drawing window size
      ss_hh = Dhh;
   }
   
   g_timeout_add(100,ss_timerfunc,0);                                            //  start timer for image changes
   return;
}


//  dialog event function - file chooser for images to show and music file

int slideshow_dialog_event(zdialog *zd, cchar *event)
{
   char     *file, *pp;
   char     countmess[50], sfile[200];

   if (strmatch(event,"focus"))
      F1_help_topic = "slide_show";

   if (strmatch(event,"enter")) zd->zstat = 1;                                   //  [proceed]
   if (strmatch(event,"escape")) zd->zstat = 2;                                  //  escape = cancel                    15.07

   if (zd->zstat == 1) {                                                         //  [proceed]
      if (ss_Nfiles) return 1;
      zmessageACK(Mwin,ZTX("invalid album"));                                    //  diagnose and keep dialog open      15.01
      zd->zstat = 0;
      return 1;
   }

   if (zd->zstat) return 1;                                                      //  cancel or [x]

   if (strmatch(event,"selectalbum"))                                            //  select a slide show album
   {
      ss_Nfiles = 0;                                                             //  reset album data
      ss_albumfile[0] = 0;
      zdialog_stuff(zd,"albumname",Bnoselection);
      zdialog_stuff(zd,"nfiles",Bnoimages);

      file = zgetfile(ZTX("open album"),MWIN,"file",albums_dirk);
      if (! file) return 1;
      if (strlen(file) > 299) {
         zmessageACK(Mwin,"file name too long");
         return 1;
      }

      strcpy(ss_albumfile,file);
      pp = strrchr(ss_albumfile,'/');                                            //  get album name
      ss_albumname = pp + 1;
      zfree(file);

      ss_loadprefs();                                                            //  get slide show prefs or defaults
      if (! ss_Nfiles) return 1;

      zdialog_stuff(zd,"albumname",ss_albumname);                                //  update dialog album data
      snprintf(countmess,50,ZTX("%d images"),ss_Nfiles);
      zdialog_stuff(zd,"nfiles",countmess);
      zdialog_stuff(zd,"seconds",ss_seconds);
      zdialog_stuff(zd,"cliplim",ss_cliplimit);
      zdialog_stuff(zd,"musicfile",ss_musicfile);
      zdialog_stuff(zd,"fullscreen",ss_fullscreen);                              //  15.03
      zdialog_stuff(zd,"replay",ss_replay);

      navi::gallerytype = ALBUM;                                                 //  open gallery with slide show album
      gallery(ss_albumfile,"initF");
      m_viewmode(0,"G");
      gallery(0,"paint",0);
   }

   if (strmatch(event,"browse")) {                                               //  browse for music file
      pp = ss_musicfile;
      pp = zgetfile(ZTX("Select music file"),MWIN,"file",pp);
      if (! pp) pp = zstrdup("none");
      zdialog_stuff(zd,"musicfile",pp);
      zfree(pp);
   }
   
   if (strmatch(event,"shortcuts")) {                                            //  16.01
      snprintf(sfile,200,"%s/images/slide-show-keys.jpg",get_zdatadir());
      popup_image(sfile,MWIN,1,267);
   }

   if (! ss_Nfiles) return 1;

   if (strmatch(event,"transprefs")) ss_transprefs_dialog();                     //  edit transition preferences
   if (strmatch(event,"imageprefs")) ss_imageprefs_dialog();                     //  edit image preferences

   return 1;
}


//  set transitions preferences for specific slide show

void ss_transprefs_dialog()
{
   int  transprefs_dialog_event(zdialog *zd, cchar *event);

   zdialog     *zd;
   int         ii, jj, zstat;
   char        nameii[SSNF], enabii[SSNF], slowii[SSNF], prefii[SSNF];
   cchar       *randmess = ZTX("select random (if 2+ enabled)");

/***
          _______________________________________________
         |        Transition Preferences                 |
         |                                               |
         |  Settings File [load] [save]                  |
         |                                               |
         |  [x] select random (if 2+ enabled)            |
         |  transition    enabled  slowdown  preference  |
         |  instant         [x]     [ 1 ]      [ 10 ]    |
         |  fade-in         [x]     [ 2 ]      [ 20 ]    |
         |  roll-right      [x]     [ 3 ]      [  0 ]    |
         |  ....            ...     ...        ...       |
         |                                               |
         |                               [done] [cancel] |
         |_______________________________________________|

***/

   if (! ss_Nfiles) {
      zmessageACK(Mwin,ZTX("invalid album"));
      return;
   }

   zd = zdialog_new(ZTX("Transition Preferences"),Mwin,Bdone,Bcancel,null);
   zdialog_set_decorated(zd,0);                                                  //  allow drag off-screen window       16.08
   zdialog_add_widget(zd,"hbox","hbfile","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labfile","hbfile",ZTX("Settings File"),"space=3");
   zdialog_add_widget(zd,"button","load","hbfile",Bload,"space=3");
   zdialog_add_widget(zd,"button","save","hbfile",Bsave,"space=3");
   zdialog_add_widget(zd,"hbox","hbrand","dialog");
   zdialog_add_widget(zd,"check","rand","hbrand",randmess,"space=3");
   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,"vbox","vb3","hb1",0,"space=3|homog");
   zdialog_add_widget(zd,"vbox","vb4","hb1",0,"space=3|homog");
   zdialog_add_widget(zd,"label","labname","vb1",ZTX("transition"));
   zdialog_add_widget(zd,"label","labenab","vb2",ZTX("enabled"));
   zdialog_add_widget(zd,"label","labslow","vb3",ZTX("slowdown"));
   zdialog_add_widget(zd,"label","labpref","vb4",ZTX("preference"));

   zdialog_stuff(zd,"rand",ss_random);                                           //  stuff random checkbox

   for (ii = 0; ii < SSNF; ii++) {                                               //  build input dialog for transition prefs
      snprintf(nameii,SSNF,"name_%d",ii);
      snprintf(enabii,SSNF,"enab_%d",ii);
      snprintf(slowii,SSNF,"slow_%d",ii);
      snprintf(prefii,SSNF,"pref_%d",ii);
      zdialog_add_widget(zd,"label",nameii,"vb1","transition");
      zdialog_add_widget(zd,"check",enabii,"vb2",0,"size=3");
      zdialog_add_widget(zd,"spin",slowii,"vb3","0|99|1|1","size=3");
      zdialog_add_widget(zd,"spin",prefii,"vb4","0|99|1|10","size=3");
      zdialog_stuff(zd,nameii,ZTX(ss_trantab[ii].tranname));                     //  stuff current transition prefs     15.09
      zdialog_stuff(zd,enabii,ss_trantab[ii].enabled);
      zdialog_stuff(zd,slowii,ss_trantab[ii].slowdown);
      zdialog_stuff(zd,prefii,ss_trantab[ii].preference);
   }

   zdialog_run(zd,transprefs_dialog_event);                                      //  run dialog, wait for completion
   zstat = zdialog_wait(zd);
   
   if (zstat != 1) {
      zdialog_free(zd);
      return;
   }
   
   zdialog_fetch(zd,"rand",ss_random);                                           //  get mode, 0/1 = sequential/random

   for (ii = 0; ii < SSNF; ii++) {                                               //  get all preferences
      snprintf(enabii,SSNF,"enab_%d",ii);
      snprintf(slowii,SSNF,"slow_%d",ii);
      snprintf(prefii,SSNF,"pref_%d",ii);
      zdialog_fetch(zd,enabii,ss_trantab[ii].enabled);
      zdialog_fetch(zd,slowii,ss_trantab[ii].slowdown);
      zdialog_fetch(zd,prefii,ss_trantab[ii].preference);
   }

   zdialog_free(zd);

   ss_saveprefs();                                                               //  update preferences file

   for (ii = jj = 0; ii < SSNF; ii++) {                                          //  initialize list of enabled
      if (ss_trantab[ii].enabled) {                                              //    and last used transition types
         ss_Tused[jj] = ii;
         jj++;
      }
      ss_Tlast[ii] = 0;
   }

   ss_Nused = jj;                                                                //  no. enabled transition types
   ss_Tnext = 0;                                                                 //  next one to use (first)

   return;
}


//  transition prefs dialog event and completion function

int transprefs_dialog_event(zdialog *zd, cchar *event)
{
   int transprefs_load(FILE *fid);
   int transprefs_save(FILE *fid);
   
   FILE     *fid;
   int      err, ii;
   char     *file;
   char     nameii[SSNF], enabii[SSNF], slowii[SSNF], prefii[SSNF];

   if (strmatch(event,"load"))                                                   //  load trans prefs from a file       16.01
   {
      file = zgetfile("open",MWIN,"file",slideshow_trans_dirk,0);                //  open trans prefs file
      if (! file) return 1;
      fid = fopen(file,"r");
      if (! fid) {
         zmessageACK(Mwin,ZTX("invalid file"));
         return 1;
      }

      err = transprefs_load(fid);                                                //  load file into dialog
      fclose(fid);
      if (err) return 1;

      zdialog_stuff(zd,"rand",ss_random);                                        //  stuff random/sequential mode

      for (ii = 0; ii < SSNF; ii++) {                                            //  build input dialog for transition prefs
         snprintf(nameii,SSNF,"name_%d",ii);
         snprintf(enabii,SSNF,"enab_%d",ii);
         snprintf(slowii,SSNF,"slow_%d",ii);
         snprintf(prefii,SSNF,"pref_%d",ii);
         zdialog_stuff(zd,nameii,ZTX(ss_trantab[ii].tranname));                  //  stuff current transition prefs     15.09
         zdialog_stuff(zd,enabii,ss_trantab[ii].enabled);
         zdialog_stuff(zd,slowii,ss_trantab[ii].slowdown);
         zdialog_stuff(zd,prefii,ss_trantab[ii].preference);
      }
   }

   if (strmatch(event,"save"))                                                   //  save trans prefs to a file         16.01
   {
      zdialog_fetch(zd,"rand",ss_random);                                        //  get random/sequential mode

      for (ii = 0; ii < SSNF; ii++) {                                            //  get all preferences
         snprintf(enabii,SSNF,"enab_%d",ii);
         snprintf(slowii,SSNF,"slow_%d",ii);
         snprintf(prefii,SSNF,"pref_%d",ii);
         zdialog_fetch(zd,enabii,ss_trantab[ii].enabled);
         zdialog_fetch(zd,slowii,ss_trantab[ii].slowdown);
         zdialog_fetch(zd,prefii,ss_trantab[ii].preference);
      }

      file = zgetfile("open",MWIN,"save",slideshow_trans_dirk,0);                //  open trans prefs file
      if (! file) return 1;
      fid = fopen(file,"w");
      if (! fid) {
         zmessageACK(Mwin,ZTX("invalid file"));
         return 1;
      }

      transprefs_save(fid);                                                      //  save dialog to file
      fclose(fid);
   }

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


//  load transition prefs from a file
//  returns 0 = OK, +N = error

int transprefs_load(FILE *fid)
{
   char        *pp, buff[XFCC];
   int         ii, jj, nn;
   char        tranname[32];
   int         n1, n2, n3;
   
   pp = fgets_trim(buff,XFCC,fid,1);
   if (! pp) goto format_error;

   nn = sscanf(buff,"random %d ",&ss_random);
   if (nn != 1) goto format_error;

   while (true)
   {
      pp = fgets_trim(buff,XFCC,fid,1);
      if (! pp) break;
      nn = sscanf(buff,"%s %d %d %d ",tranname,&n1,&n2,&n3);                     //  tranname        N  NN  NN
      if (nn != 4) goto format_error;                                            //  (enabled 0-1  slowdown 0-99  pref. 0-99)
      for (ii = 0; ii < SSNF; ii++)
         if (strmatch(tranname,ss_trantab[ii].tranname)) break;
      if (ii == SSNF) goto format_error;
      ss_trantab[ii].enabled = n1;
      ss_trantab[ii].slowdown = n2;
      ss_trantab[ii].preference = n3;
   }

   for (ii = jj = 0; ii < SSNF; ii++) {                                          //  initialize list of enabled
      if (ss_trantab[ii].enabled) {                                              //    transition types
         ss_Tused[jj] = ii;
         jj++;
      }
   }

   ss_Nused = jj;                                                                //  no. enabled transition types
   return 0;

format_error:
   zmessageACK(Mwin,ZTX("file format error: \n %s"),buff);
   return 1;
}


//  save transition prefs to a file

int transprefs_save(FILE *fid)
{
   fprintf(fid,"random %d \n",ss_random);

   for (int ii = 0; ii < SSNF; ii++)
      fprintf(fid,"%s %d %d %d \n", ss_trantab[ii].tranname,
              ss_trantab[ii].enabled, ss_trantab[ii].slowdown,
              ss_trantab[ii].preference);
   return 0;
}


//  set image preferences for specific slide show

void ss_imageprefs_dialog()
{
   int ss_imageprefs_dialog_event(zdialog *zd, cchar *event);

   zdialog  *zd;
   int      ii;
   char     *pp, ztypN[8] = "ztypN";
   char     zoomloc[40];

/***
       _______________________________________________
      |        Image Preferences                      |
      |                                               |
      |  Image File: /path.../filename.jpg            |
      |  Play Tone [x]                                |
      |  Show Caption  [  0 ] Seconds                 |
      |  Show Comments [ 12 ] Seconds                 |
      |  Wait [ 3 ] Seconds                           |
      |  Zoom  (o) none  (o) zoomin  (o) zoomout      |
      |  Zoom size (x) [ 2.5 ]   Steps [ 200 ]        |
      |  [Zoom Center]  position: 0  0                |
      |  Wait [ 6 ] Seconds                           |
      |  Transition [ rotate |v]                      |
      |                                               |
      |                                        [Done] |                          //  no [cancel] - widgets apply instantly
      |_______________________________________________|

***/

   if (! ss_Nfiles) {
      zmessageACK(Mwin,ZTX("invalid album"));
      return;
   }

   m_viewmode(0,"G");                                                            //  gallery view                       15.01

   zd = zdialog_new(ZTX("Image Preferences"),Mwin,Bdone,null);
   zd_ss_imageprefs = zd;

   zdialog_add_widget(zd,"hbox","hbimf","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","labimf","hbimf",ZTX("Image File:"),"space=3");
   zdialog_add_widget(zd,"label","imagefile","hbimf",0,"space=3");

   zdialog_add_widget(zd,"hbox","hbpt","dialog");
   zdialog_add_widget(zd,"check","tone","hbpt",ZTX("Play tone when image shows"),"space=3");

   zdialog_add_widget(zd,"hbox","hbcap","dialog");
   zdialog_add_widget(zd,"label","labcap","hbcap",ZTX("Show image caption"),"space=3");
   zdialog_add_widget(zd,"spin","caption","hbcap","0|99|1|0","space=3");
   zdialog_add_widget(zd,"label","labsecs","hbcap",Bseconds,"space=5");

   zdialog_add_widget(zd,"hbox","hbcom","dialog");
   zdialog_add_widget(zd,"label","labcom","hbcom",ZTX("Show image comments"),"space=3");
   zdialog_add_widget(zd,"spin","comments","hbcom","0|99|1|0","space=3");
   zdialog_add_widget(zd,"label","labsecs","hbcom",Bseconds,"space=5");

   zdialog_add_widget(zd,"hbox","hbbz","dialog");
   zdialog_add_widget(zd,"label","labwait","hbbz",ZTX("Wait before zoom"),"space=3");
   zdialog_add_widget(zd,"spin","wait1","hbbz","0|99|1|0","space=3");
   zdialog_add_widget(zd,"label","labbzs2","hbbz",Bseconds,"space=5");
   
   zdialog_add_widget(zd,"hbox","hbztyp","dialog");                              //                                     15.07
   zdialog_add_widget(zd,"label","labztyp","hbztyp",ZTX("Zoom type:"),"space=3");
   zdialog_add_widget(zd,"radio","ztyp0","hbztyp",ZTX("none"),"space=3");
   zdialog_add_widget(zd,"radio","ztyp1","hbztyp",ZTX("zoom-in"),"space=3");
   zdialog_add_widget(zd,"radio","ztyp2","hbztyp",ZTX("zoom-out"),"space=3");

   zdialog_add_widget(zd,"hbox","hbz","dialog");
   zdialog_add_widget(zd,"label","labzsz","hbz",ZTX("Zoom size (x)"),"space=3");
   zdialog_add_widget(zd,"spin","zoomsize","hbz","1.0|3.0|0.1|1.0","space=3");
   zdialog_add_widget(zd,"label","space","hbz",0,"space=5");
   zdialog_add_widget(zd,"label","labzst","hbz",ZTX("Steps"),"space=3");
   zdialog_add_widget(zd,"spin","zoomsteps","hbz","50|999|1|200");

   zdialog_add_widget(zd,"hbox","hbzloc","dialog");
   zdialog_add_widget(zd,"button","zloc","hbzloc",ZTX("Zoom Center"),"space=3");
   zdialog_add_widget(zd,"label","labzloc","hbzloc","position: 50  50","space=3");

   zdialog_add_widget(zd,"hbox","hbaz","dialog");
   zdialog_add_widget(zd,"label","labwait","hbaz",ZTX("Wait after zoom"),"space=3");
   zdialog_add_widget(zd,"spin","wait2","hbaz","0|99|1|0","space=3");
   zdialog_add_widget(zd,"label","labbzs2","hbaz",Bseconds,"space=5");

   zdialog_add_widget(zd,"hbox","hbtrn","dialog");
   zdialog_add_widget(zd,"label","labtr","hbtrn",ZTX("Transition to next image"),"space=3");
   zdialog_add_widget(zd,"combo","tranname","hbtrn");

   zdialog_cb_app(zd,"tranname",ZTX("next"));                                    //  default transition                 15.09

   for (ii = 0; ii < SSNF; ii++)                                                 //  add all transitions to dropdown list
      zdialog_cb_app(zd,"tranname",ZTX(ss_trantab[ii].tranname));                //  15.09

   if (curr_file) {                                                              //  set to curr. file
      for (ii = 0; ii < ss_Nfiles; ii++)                                         //    if member of album
         if (strmatch(curr_file,ss_imagetab[ii].imagefile)) break;
      if (ii == ss_Nfiles) ii = 0;
   }
   else ii = 0;                                                                  //  else first file in album
   ss_Fcurrent = ii;
   
   ss_setzloc = 0;                                                               //  1-shot flag is off                 16.01

   pp = strrchr(ss_imagetab[ii].imagefile,'/');                                  //  stuff curr. image file data
   if (pp) zdialog_stuff(zd,"imagefile",pp+1);                                   //    into dialog
   zdialog_stuff(zd,"tone",ss_imagetab[ii].tone);
   zdialog_stuff(zd,"caption",ss_imagetab[ii].capsecs);
   zdialog_stuff(zd,"comments",ss_imagetab[ii].commsecs);
   zdialog_stuff(zd,"wait1",ss_imagetab[ii].wait1);
   ztypN[4] = ss_imagetab[ii].zoomtype + '0';
   zdialog_stuff(zd,ztypN,1);
   zdialog_stuff(zd,"zoomsize",ss_imagetab[ii].zoomsize);
   zdialog_stuff(zd,"zoomsteps",ss_imagetab[ii].zoomsteps);
   snprintf(zoomloc,40,"position: x=%02d  y=%02d",                               //  stuff zoom location if defined     16.01
            ss_imagetab[ii].zoomlocx,ss_imagetab[ii].zoomlocy);                  //    (else  x=00  y=00)
   zdialog_stuff(zd,"labzloc",zoomloc);
   zdialog_stuff(zd,"wait2",ss_imagetab[ii].wait2);
   zdialog_stuff(zd,"tranname",ZTX(ss_imagetab[ii].tranname));                   //  15.09

   zdialog_run(zd,ss_imageprefs_dialog_event);                                   //  run dialog
   zdialog_wait(zd);                                                             //  wait for completion
   zdialog_free(zd);
   zd_ss_imageprefs = 0;

   ss_saveprefs();                                                               //  save updated preferences file
   return;
}


//  image prefs dialog event and completion function

int  ss_imageprefs_dialog_event(zdialog *zd, cchar *event)
{
   int         ii, jj;
   float       ff;
   char        tranname[32];
   GdkWindow   *gdkwin;

   ii = ss_Fcurrent;                                                             //  from mouse click on thumbnail
   if (ii >= ss_Nfiles) return 1;

   if (strmatch(event,"tone")) {
      zdialog_fetch(zd,"tone",jj);
      ss_imagetab[ii].tone = jj;
   }

   if (strmatch(event,"caption")) {
      zdialog_fetch(zd,"caption",jj);
      ss_imagetab[ii].capsecs = jj;
   }

   if (strmatch(event,"comments")) {
      zdialog_fetch(zd,"comments",jj);
      ss_imagetab[ii].commsecs = jj;
   }

   if (strmatch(event,"wait1")) {
      zdialog_fetch(zd,"wait1",jj);
      ss_imagetab[ii].wait1 = jj;
   }
   
   if (strstr("ztyp0 ztyp1 ztyp2",event)) {                                      //  zoom type                          15.07
      zdialog_fetch(zd,event,jj);
      jj = event[4] - '0';
      ss_imagetab[ii].zoomtype = jj;
   }
   
   if (strmatch(event,"zoomsize")) {
      zdialog_fetch(zd,"zoomsize",ff);
      ss_imagetab[ii].zoomsize = ff;
   }

   if (strmatch(event,"zoomsteps")) {
      zdialog_fetch(zd,"zoomsteps",jj);
      ss_imagetab[ii].zoomsteps = jj;
   }

   if (strmatch(event,"zloc")) {
      ss_setzloc = 1;                                                            //  set 1-shot flag                    16.01
      gdkwin = gtk_widget_get_window(Gdrawin);
      gdk_window_set_cursor(gdkwin,dragcursor);
      poptext_mouse(ZTX("click on thumbnail to set zoom center"),20,20,0,2);
   }

   if (strmatch(event,"wait2")) {
      zdialog_fetch(zd,"wait2",jj);
      ss_imagetab[ii].wait2 = jj;
   }

   if (strmatch(event,"tranname")) {
      zdialog_fetch(zd,"tranname",tranname,32);
      if (! strmatch(tranname,ZTX("next"))) {                                    //  15.09
         for (jj = 0; jj < SSNF; jj++)
            if (strmatch(tranname,ZTX(ss_trantab[jj].tranname))) break;
         if (jj == SSNF) return 1;
         strncpy0(ss_imagetab[ii].tranname,ss_trantab[jj].tranname,32);
      }
      else strncpy0(ss_imagetab[ii].tranname,"next",32);
   }

   return 1;
}


//  response function for gallery thumbnail left-click
//  stuff image prefs dialog with data for clicked image

void ss_imageprefs_Lclick_func(int Nth)
{
   zdialog     *zd;
   int         ii;
   char        *pp, ztypN[8] = "ztypN";
   char        zoomloc[40];
   GdkWindow   *gdkwin;

   if (! clicked_file) return;

   zd = zd_ss_imageprefs;                                                        //  should not happen
   if (! zd) {
      zfree(clicked_file);
      clicked_file = 0;
      return;
   }
   
   for (ii = 0; ii < ss_Nfiles; ii++)                                            //  find clicked file in image prefs   16.06
      if (strmatch(clicked_file,ss_imagetab[ii].imagefile)) break;
   zfree(clicked_file);
   clicked_file = 0;
   if (ii == ss_Nfiles) return;                                                  //  not found, album file removed

   ss_Fcurrent = ii;

   if (ss_setzloc) {                                                             //  1-shot flag is set                 16.01
      ss_setzloc = 0;
      ss_imagetab[ii].zoomlocx = clicked_width;                                  //  set zoom-in location from
      ss_imagetab[ii].zoomlocy = clicked_height;                                 //    thumbnail click position
      gdkwin = gtk_widget_get_window(Gdrawin);
      gdk_window_set_cursor(gdkwin,0);
   }

   pp = strrchr(ss_imagetab[ii].imagefile,'/');                                  //  stuff image data into dialog
   if (pp) zdialog_stuff(zd,"imagefile",pp+1);
   zdialog_stuff(zd,"tone",ss_imagetab[ii].tone);
   zdialog_stuff(zd,"caption",ss_imagetab[ii].capsecs);
   zdialog_stuff(zd,"comments",ss_imagetab[ii].commsecs);
   zdialog_stuff(zd,"wait1",ss_imagetab[ii].wait1);
   ztypN[4] = '0' + ss_imagetab[ii].zoomtype;
   zdialog_stuff(zd,ztypN,1);
   zdialog_stuff(zd,"zoomsize",ss_imagetab[ii].zoomsize);
   zdialog_stuff(zd,"zoomsteps",ss_imagetab[ii].zoomsteps);
   snprintf(zoomloc,40,"position: x=%02d  y=%02d",                               //  stuff zoom location                16.01
            ss_imagetab[ii].zoomlocx,ss_imagetab[ii].zoomlocy);
   zdialog_stuff(zd,"labzloc",zoomloc);
   zdialog_stuff(zd,"wait2",ss_imagetab[ii].wait2);
   zdialog_stuff(zd,"tranname",ZTX(ss_imagetab[ii].tranname));                   //  15.09

   return;
}


/***********************  preferences file format  ******************************

   global data:
   seconds: NN                      0-999
   cliplimit: NN                    0-50
   random: N                        0-1
   musicfile: /path.../file.ogg     music file or "none"
   fullscreen:                      0-1
   replay:                          0-1

   transitions data:
   tranname N NN NN                 tranname enabled slowdown preference
   tranname N NN NN                            0-1     0-99     0-99
   ...

   images data:
   imagefile: /path.../file.jpg
   tone: N                          0-1
   caption: NN                      seconds to show caption
   comments: NN                     seconds to show comments
   wait1: NN                        seconds to wait 0-99
   zoomtype: N                      0/1/2 = none/zoomin/zoomout                  //  added                              15.07
   zoomsize: N.N                    1.0 - 3.0
   zoomsteps: NNN                   100-999
   zoomloc: NN NN                   20-80 20-80
   wait2: NN                        seconds to wait 0-99
   tranname: aaaaaa                 transition name or "next"
   ...

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


//  Load all data for a specific slide show from a slide show preferences file.
//  Set defaults if no data previously defined.

void ss_loadprefs() 
{
   FILE        *fid;
   char        buff[XFCC];
   char        prefsfile[300], *pp;
   int         ii, jj, nn, format;
   FTYPE       ftype;
   char        tranname[32];
   int         n1, n2, n3;
   float       ff;

   for (ii = 0; ii < ss_Nfiles; ii++) {                                          //  free prior image data if any
      pp = ss_imagetab[ii].imagefile;
      if (pp) zfree(pp);
      ss_imagetab[ii].imagefile = 0;
   }

   ss_Nfiles = 0;

   fid = fopen(ss_albumfile,"r");                                                //  open album file
   if (! fid) {
      zmessageACK(Mwin,ZTX("invalid album"));
      return;
   }

   for (ii = 0; ii < SSMAXI; ) {                                                 //  read all image file names
      pp = fgets_trim(buff,XFCC,fid);
      if (! pp) break;
      ftype = image_file_type(pp);                                               //  screen out deleted image files
      if (ftype != IMAGE) continue;
      ss_imagetab[ii].imagefile = zstrdup(pp);                                   //  add to image table
      ii++;
   }

   fclose(fid);
   ss_Nfiles = ii;

   if (! ss_Nfiles) {
      zmessageACK(Mwin,ZTX("invalid album"));
      return;
   }

   navi::gallerytype = ALBUM;                                                    //  open gallery with slide show album
   gallery(ss_albumfile,"initF");
   m_viewmode(0,"G");
   gallery(0,"paint",0);

   ss_seconds = 3;                                                               //  defaults: image display time
   ss_cliplimit = 0;                                                             //  image clip limit = no clipping
   strcpy(ss_musicfile,"none");                                                  //  no music file

   for (ii = 0; ii < SSNF; ii++)                                                 //  initialize transitions table
      ss_trantab[ii] = ss_trantab_default[ii];                                   //    with default preferences

   for (ii = 0; ii < SSNF; ii++) {
      ss_Tused[ii] = ii;                                                         //  all transition types are used
      ss_Tlast[ii] = 0;                                                          //  last used list is empty
   }

   ss_random = 0;                                                                //  random transitions = NO
   ss_Nused = SSNF;                                                              //  used transitions = all
   ss_Tnext = 0;                                                                 //  next = first

   for (ii = 0; ii < ss_Nfiles; ii++) {                                          //  initialize image table with defaults
      ss_imagetab[ii].tone = 0;
      ss_imagetab[ii].capsecs = 0;
      ss_imagetab[ii].commsecs = 0;
      ss_imagetab[ii].wait1 = 0;
      ss_imagetab[ii].zoomtype = 0;                                              //  no zoom-in                         15.07
      ss_imagetab[ii].zoomsize = 1.0;
      ss_imagetab[ii].zoomsteps = 200;
      ss_imagetab[ii].zoomlocx = 50;
      ss_imagetab[ii].zoomlocy = 50;
      ss_imagetab[ii].wait2 = 0;
      strcpy(ss_imagetab[ii].tranname,"next");
   }

   snprintf(prefsfile,300,"%s/%s",slideshow_dirk,ss_albumname);
   fid = fopen(prefsfile,"r");                                                   //  open slide show prefs file
   if (! fid) return;

   format = 0;

   while (true)
   {
      pp = fgets_trim(buff,XFCC,fid,1);
      if (! pp) break;

      if (strmatchN(pp,"global data:",12)) {
         format = 1;
         continue;
      }

      if (strmatchN(pp,"transitions data:",17)) {
         format = 2;
         continue;
      }

      if (strmatchN(pp,"images data:",12)) {
         format = 3;
         continue;
      }

      if (format == 1)                                                           //  overall preferences
      {
         if (strmatchN(pp,"seconds: ",9)) {                                      //  seconds: NN   seconds to show each image
            ss_seconds = atoi(pp+9);
            continue;
         }

         if (strmatchN(pp,"cliplimit: ",11)) {                                   //  cliplimit: NN   margin clip limit  0-50 %
            ss_cliplimit = atoi(pp+11);
            continue;
         }

         if (strmatchN(pp,"random: ",8)) {                                       //  random: N   0-1 = seq./random transactions
            ss_random = atoi(pp+8);
            continue;
         }

         if (strmatchN(pp,"musicfile: ",11)) {                                   //  musicfile: /directory/.../musicfile.ogg
            strncpy0(ss_musicfile,pp+11,500);
            continue;
         }

         if (strmatchN(pp,"fullscreen: ",12)) {                                  //  fullscreen: N   0-1 = no / full screen
            ss_fullscreen = atoi(pp+12);                                         //  15.03
            continue;
         }

         if (strmatchN(pp,"replay: ",8)) {                                       //  random: N   0-1 = no / replay after end
            ss_replay = atoi(pp+8);                                              //  15.03
            continue;
         }
      }

      if (format == 2)                                                           //  transition preferences
      {
         nn = sscanf(buff,"%s %d %d %d ",tranname,&n1,&n2,&n3);                  //  tranname        N  NN  NN
         if (nn != 4) goto format_error;                                         //  (enabled 0-1  slowdown 0-99  pref. 0-99)
         for (ii = 0; ii < SSNF; ii++)
            if (strmatch(tranname,ss_trantab[ii].tranname)) break;
         if (ii == SSNF) goto format_error;
         ss_trantab[ii].enabled = n1;
         ss_trantab[ii].slowdown = n2;
         ss_trantab[ii].preference = n3;
      }

      if (format == 3)                                                           //  image file preferences
      {
         if (strmatchN(pp,"imagefile: ",11)) {                                   //  set image file for subsequent recs
            pp += 11;
            if (*pp != '/') goto format_error;
            for (ii = 0; ii < ss_Nfiles; ii++)                                   //  search album for matching image
               if (strmatch(pp,ss_imagetab[ii].imagefile)) break;
            if (ii == ss_Nfiles) ii = -1;                                        //  if not found, set no curr. image
            continue;
         }

         if (ii < 0) continue;                                                   //  ignore recs following invalid image file

         if (strmatchN(pp,"tone: ",6)) {                                         //  tone: N    0 or 1 = play tone
            nn = atoi(pp+6);
            ss_imagetab[ii].tone = nn;
            continue;
         }

         if (strmatchN(pp,"caption: ",9)) {                                      //  caption: NN   seconds to show caption
            nn = atoi(pp+9);
            ss_imagetab[ii].capsecs = nn;
            continue;
         }

         if (strmatchN(pp,"comments: ",10)) {                                    //  comments: NN   seconds to show comments
            nn = atoi(pp+10);
            ss_imagetab[ii].commsecs = nn;
            continue;
         }

         if (strmatchN(pp,"wait1: ",7)) {                                        //  wait1: NN   seconds before zoom-in
            nn = atoi(pp+7);
            ss_imagetab[ii].wait1 = nn;
            continue;
         }
         
         if (strmatchN(pp,"zoomtype: ",10)) {                                    //  zoomtype: N    zoom type           15.07
            nn = atoi(pp+10);
            ss_imagetab[ii].zoomtype = nn;
            continue;
         }

         if (strmatchN(pp,"zoomsize: ",10)) {                                    //  zoomsize: N.N   1.0 - 3.0 = 3x     15.02
            ff = atof(pp+10);                                                    //  ff float                           15.12
            ss_imagetab[ii].zoomsize = ff;
            continue;
         }

         if (strmatchN(pp,"zoomsteps: ",11)) {                                   //  zoomsteps: NNN   zoom-in steps 100-999
            nn = atoi(pp+11);
            ss_imagetab[ii].zoomsteps = nn;
            continue;
         }

         if (strmatchN(pp,"zoomloc: ",9)) {                                      //  zoomloc: NN NN   zoom-in location
            nn = strtol(pp+9,&pp,10);                                            //  (20-80% of image width and height)
            ss_imagetab[ii].zoomlocx = nn;
            nn = atoi(pp);
            ss_imagetab[ii].zoomlocy = nn;
            continue;
         }

         if (strmatchN(pp,"wait2: ",7)) {                                        //  wait2: NN   seconds after zoom-in
            nn = atoi(pp+7);
            ss_imagetab[ii].wait2 = nn;
            continue;
         }

         if (strmatchN(pp,"tranname: ",10)) {                                    //  transaction to next image
            strncpy0(ss_imagetab[ii].tranname,pp+10,32);
            continue;
         }
      }
   }

   fclose(fid);

   for (ii = jj = 0; ii < SSNF; ii++) {                                          //  initialize list of enabled
      if (ss_trantab[ii].enabled) {                                              //    transition types
         ss_Tused[jj] = ii;
         jj++;
      }
   }

   ss_Nused = jj;                                                                //  no. enabled transition types
   return;

format_error:
   zmessageACK(Mwin,ZTX("file format error: \n %s"),buff);
   fclose(fid);
   return;
}


//  Save all data for a specific slide show to a slide show preferences file.

void ss_saveprefs()
{
   FILE        *fid;
   char        prefsfile[300];
   int         ii;

   if (! ss_Nfiles) {
      zmessageACK(Mwin,ZTX("invalid album"));
      return;
   }

   snprintf(prefsfile,300,"%s/%s",slideshow_dirk,ss_albumname);
   fid = fopen(prefsfile,"w");                                                   //  open slide show prefs file
   if (! fid) {
      zmessageACK(Mwin,strerror(errno));
      return;
   }

   fprintf(fid,"global data: \n");
   fprintf(fid,"seconds: %d \n",ss_seconds);
   fprintf(fid,"cliplimit: %d \n",ss_cliplimit);
   fprintf(fid,"random: %d \n",ss_random);
   fprintf(fid,"musicfile: %s \n",ss_musicfile);
   fprintf(fid,"fullscreen: %d \n",ss_fullscreen);
   fprintf(fid,"replay: %d \n",ss_replay);

   fprintf(fid,"transitions data: \n");

   for (ii = 0; ii < SSNF; ii++)
      fprintf(fid,"%s %d %d %d \n", ss_trantab[ii].tranname,
              ss_trantab[ii].enabled, ss_trantab[ii].slowdown,
              ss_trantab[ii].preference);

   fprintf(fid,"images data: \n");

   for (ii = 0; ii < ss_Nfiles; ii++)
   {
      fprintf(fid,"imagefile: %s \n",ss_imagetab[ii].imagefile);
      fprintf(fid,"tone: %d \n",ss_imagetab[ii].tone);
      fprintf(fid,"caption: %d \n",ss_imagetab[ii].capsecs);
      fprintf(fid,"comments: %d \n",ss_imagetab[ii].commsecs);
      fprintf(fid,"wait1: %d \n",ss_imagetab[ii].wait1);
      fprintf(fid,"zoomtype: %d \n",ss_imagetab[ii].zoomtype);                   //  15.07
      fprintf(fid,"zoomsize: %.1f \n",ss_imagetab[ii].zoomsize);
      fprintf(fid,"zoomsteps: %d \n",ss_imagetab[ii].zoomsteps);
      fprintf(fid,"zoomloc: %d %d \n",ss_imagetab[ii].zoomlocx,ss_imagetab[ii].zoomlocy);
      fprintf(fid,"wait2: %d \n",ss_imagetab[ii].wait2);
      fprintf(fid,"tranname: %s \n",ss_imagetab[ii].tranname);
   }

   fclose(fid);

   ss_loadprefs();                                                               //  reload to sync poss. album edits   16.06
   return;
}


//  Show next slide when time is up or user navigates with arrow keys.
//  Cycles every 0.1 seconds when slide show is active.

int ss_timerfunc(void *)                                                         //  overhauled 15.02
{
   int            ii, jj;
   int            capsecs, commsecs, mode;
   char           *file;
   double         cctime;
   static int     pmode;
   static double  starttime;
   float          zoom;
   int            zoomtype;
   
   ii = ss_Fcurrent;                                                             //  current image file in album

   if (strmatch(ss_state,"end")) {                                               //  end of image list                  16.07
      if (ss_replay) {
         ii = ss_Fcurrent = 0;                                                   //  if auto replay, back to first image
         ss_state = "show";
      }
      else {
         file = ss_imagetab[ii].imagefile;                                       //  last file shown --> current file   16.08
         f_open(file);
         zmessage_post(Mwin,3,ZTX("END (press Escape to exit)"));
         ss_state = "end2";
         return 1;                                                               //  wait for escape
      }
   }
   
   if (ss_escape) {                                                              //  end slide show, return to dialog   16.08
      ss_escape = 0;
      if (ss_pxbold) g_object_unref(ss_pxbold);                                  //  free memory
      if (ss_pxbnew) g_object_unref(ss_pxbnew);
      ss_pxbold = ss_pxbnew = 0;
      if (*ss_musicfile == '/') shell_quiet("pulseaudio --kill");                //  kill music if any                  15.02
      win_unfullscreen();                                                        //  restore old window size, menu etc.
      ss_fullscreen = 0;
      Fslideshow = 0;                                                            //  reset flags
      Fblowup = 0;
      m_slideshow(0,0);                                                          //  return to slide show dialog
      return 0;                                                                  //  stop the timer
   }

   if (strmatch(ss_state,"end2")) return 1;                                      //  waiting for escape                 16.08

   if (ss_spacebar) {                                                            //  pause/resume
      ss_spacebar = 0;
      ss_blank = 0;
      if (! strmatch(ss_state,"pause")) {                                        //  pause
         ss_state = "pause";
         f_open(ss_imagetab[ii].imagefile);                                      //  open current image                 16.05
      }
      else {                                                                     //  resume
         if (zdmagnify) zdialog_send_event(zdmagnify,"kill");                    //  if magnify active, terminate 
         ss_Flast = ss_Fcurrent;
         ss_Fcurrent++;
         if (ss_Fcurrent == ss_Nfiles) ss_Fcurrent = 0;
         ss_state = "instant";
      }
      return 1;
   }

   if (ss_Xkey) {                                                                //  KB X key 
      ss_Xkey = 0;
      if (! strmatch(ss_state,"pause")) return 1;                                //  ignore if not paused
      m_magnify(0,0);
      return 1;
   }
   
   if (ss_Bkey) {                                                                //  blank/unblank screen
      ss_Bkey = 0;
      ss_blank = 1 - ss_blank;
      if (ss_blank) {
         ss_blankwindow();
         ss_state = "pause";
      }
      else ss_state = "instant";
      return 1;
   }

   if (ss_Larrow) {                                                              //  left arrow, back one image
      ss_Larrow = 0;
      ss_blank = 0;
      ss_Flast = ss_Fcurrent;
      ss_Fcurrent--;
      if (ss_Fcurrent < 0) ss_Fcurrent = ss_Nfiles-1;
      ss_state = "arrow";
      return 1;
   }

   if (ss_Rarrow) {                                                              //  right arrow, forward one image
      ss_Rarrow = 0;
      ss_blank = 0;
      ss_Flast = ss_Fcurrent;
      ss_Fcurrent++;
      if (ss_Fcurrent == ss_Nfiles) ss_Fcurrent = 0;
      ss_state = "arrow";
      return 1;
   }

   if (ss_Nkey) {                                                                //  N key, transition + forward        16.01
      ss_Nkey = 0;
      ss_blank = 0;
      ss_Flast = ss_Fcurrent;
      ss_Fcurrent++;
      if (ss_Fcurrent == ss_Nfiles) ss_Fcurrent = 0;
      ss_state = "shownow";
      return 1;
   }

   if (strmatch(ss_state,"pause")) return 1;                                     //  do nothing

   if (strmatch(ss_state,"first"))                                               //  first image
      ss_state = "instant";
      
   if (strstr("show shownow instant arrow",ss_state))                            //  show or instant show               16.01
   {
      ss_oldfile = ss_newfile;                                                   //  old file = new
      if (ss_pxbold) g_object_unref(ss_pxbold);
      ss_pxbold = ss_pxbnew;                                                     //  old pixbuf = new
      ss_newfile = ss_imagetab[ii].imagefile;                                    //  new current file
      ss_pxbnew = ss_loadpxb(ss_newfile);                                        //  new pixbuf
      if (! ss_pxbnew) {
         ss_escape = 1;                                                          //  failure, quit slide show           16.06
         return 1;
      }

      zoom = ss_imagetab[ii].zoomsize;                                           //  zoom size
      zoomtype = ss_imagetab[ii].zoomtype;                                       //  zoom type, none or in or out
      ss_zoomlocx = ss_imagetab[ii].zoomlocx;                                    //  target location for final center
      ss_zoomlocy = ss_imagetab[ii].zoomlocy;
      if (zoom > 1 && zoomtype == 2) {                                           //  next image will be zoomed out      15.07
         g_object_unref(ss_pxbnew);
         ss_zoom_posn(ss_newfile,1,zoom,0);                                      //  initial image = zoomed image
         ss_pxbnew = ss_zoom_posn(0,2,zoom,0);
         ss_zoom_posn(0,3,0,0);
      }
      
      if (strstr("instant arrow",ss_state))                                      //  skip transition, show now
         ss_instant();
      else {
         jj = ss_nextrans();                                                     //  select next transition type
         ss_slowdown = ss_trantab[jj].slowdown;                                  //  set slowdown factor for transition
         ss_trantab[jj].func();                                                  //  call transition function
      }

      if (! ss_fullscreen)                                                       //  if not full screen mode,           16.01
         gtk_window_set_title(MWIN,ss_newfile);                                  //    put filename in window title bar
      
      if (strmatch("arrow",ss_state)) {                                          //  show immediately
         ss_instant();
         ss_state = "pause";
         return 1;
      }

      if (strmatch("shownow",ss_state)) {                                        //  show immediately                   16.01
         ss_state = "instant";
         return 1;
      }

      if (ss_imagetab[ii].tone)                                                  //  play tone if specified             15.02
         shell_quiet("paplay %s/slideshow-tone.oga &",slideshow_dirk);

      ss_state = "wait1";                                                        //  wait before zoom-in
      starttime = get_seconds();
      if (ss_imagetab[ii].capsecs > ss_imagetab[ii].commsecs)
         cctime = ss_imagetab[ii].capsecs;                                       //  add larger of caption/comments time
      else cctime = ss_imagetab[ii].commsecs;
      ss_timer = starttime + cctime + ss_imagetab[ii].wait1;                     //  time to wait
      return 1;
   }

   if (strmatch(ss_state,"wait1")) {                                             //  show caption/comments
      cctime = get_seconds() - starttime;                                        //    for specified intervals          15.02
      capsecs = ss_imagetab[ii].capsecs;
      commsecs = ss_imagetab[ii].commsecs;
      mode = 0;
      if (capsecs && capsecs > cctime) mode = 1;                                 //  show caption
      if (commsecs && commsecs > cctime) mode += 2;                              //  show comments or both
      if ((capsecs || commsecs) && ! mode) mode = 4;                             //  erase them
      if (mode != pmode) ss_showcapcom(mode);
      pmode = mode;
   }

   if (strmatch(ss_state,"wait1")) {
      if (get_seconds() < ss_timer) return 1;                                    //  wait for zoom-in time
      ss_zoomsize = ss_imagetab[ii].zoomsize;                                    //  1-3x = no zoom to 3x zoom
      ss_zoomsteps = ss_imagetab[ii].zoomsteps;                                  //  zoom steps
      ss_zoomlocx = ss_imagetab[ii].zoomlocx;                                    //  target location for final center
      ss_zoomlocy = ss_imagetab[ii].zoomlocy;                                    //  (0-100% of image, 50/50 = middle)
      if (ss_zoomsize > 1.0) {
         if (ss_imagetab[ii].zoomtype == 1) ss_zoomin();                         //  zoomin or zoomout                  15.07
         if (ss_imagetab[ii].zoomtype == 2) ss_zoomout();
      }
      ss_state = "wait2";                                                        //  wait for next image
      ss_timer = get_seconds() + ss_imagetab[ii].wait2;
      return 1;
   }

   if (strmatch(ss_state,"wait2")) {
      if (get_seconds() < ss_timer) return 1;                                    //  wait until next image time
      ss_state = "sswait";
      ss_timer = get_seconds() + ss_seconds;
      return 1;
   }

   if (strmatch(ss_state,"sswait")) {                                            //  global time interval
      if (get_seconds() < ss_timer) return 1;                                    //  wait for my time
      ss_Flast = ss_Fcurrent;
      if (ss_Fcurrent == ss_Nfiles - 1)                                          //  was last image file
         ss_state = "end";
      else {
         ss_Fcurrent++;                                                          //  show next
         ss_state = "show";
      }
      return 1;
   }

   return 1;
}


//  select next transition type to use
//  mode = sequential: use each enabled transition type in sequence
//  mode = random: exclude recently used, choose random from remaining

int ss_nextrans()
{
   int      ii, jj, maxii, maxjj, next;
   float    maxrank, rank;

   ii = ss_Flast;                                                                //  transition type from last image

   if (! strmatch(ss_imagetab[ii].tranname,"next")) {                            //  image transition not "next"
      for (jj = 0; jj < SSNF; jj++)
         if (strmatch(ss_trantab[jj].tranname,ss_imagetab[ii].tranname)) break;
      if (jj < SSNF) {
         next = jj;                                                              //  assigned transition type
         for (ii = ss_Nused - 1; ii > 0; ii--)                                   //  >> most recently used
            ss_Tlast[ii] = ss_Tlast[ii-1];
         ss_Tlast[0] = next;
         return next;
      }
   }

   if (ss_Nused < 2 || ss_random == 0)                                           //  few enabled transition types       16.01
   {                                                                             //    or sequential mode
      ss_Tnext++;
      if (ss_Tnext == ss_Nused) ss_Tnext = 0;                                    //  select transition types sequentially
      next = ss_Tused[ss_Tnext];
   }

   else                                                                          //  select transition types randomly
   {
      maxrank = 0;
      maxii = 0;
      maxjj = ss_Nused / 2;                                                      //  most recently used to exclude
      if (maxjj > 4) maxjj = 4;                                                  //  max. 4

      for (ii = 0; ii < ss_Nused; ii++)                                          //  search enabled transitions
      {
         for (jj = 0; jj < maxjj; jj++)                                          //  exclude most recently used 50%
            if (ss_Tused[ii] == ss_Tlast[jj]) break;
         if (jj < maxjj) continue;
         jj = ss_Tused[ii];
         rank = ss_trantab[jj].preference * drandz();                            //  rank = preference * random value
         if (rank > maxrank) {
            maxrank = rank;                                                      //  remember highest rank
            maxii = ii;
         }
      }

      next = ss_Tused[maxii];                                                    //  transition to use

      for (ii = ss_Nused - 1; ii > 0; ii--)                                      //  make it most recent
         ss_Tlast[ii] = ss_Tlast[ii-1];
      ss_Tlast[0] = next;
   }

   return next;
}


//  write captions and comments at the top of the image
//  mode: 1 write caption only
//        2 write comments only
//        3 write both
//        4 clear both

void ss_showcapcom(int mode)                                                     //  15.02
{
   cchar        *keynames[2] = { iptc_caption_key, exif_comment_key };
   char         *keyvals[2];
   char         caption[200], comments[200];
   static char  text[402];
   PIXBUF       *pxbclear;

   static PangoFontDescription   *pangofont = null;
   static PangoLayout            *pangolayout = null;
   static int                    plww, plhh;

   if (plww) {                                                                   //  clear previous text
      ss_cr = gdk_cairo_create(gdkwin);                                          //  (restore image)
      pxbclear = gdk_pixbuf_new_subpixbuf(ss_pxbnew,0,0,plww+10,plhh+10);
      gdk_cairo_set_source_pixbuf(ss_cr,pxbclear,0,0);
      cairo_paint(ss_cr);
      cairo_destroy(ss_cr);
      zmainloop();
      g_object_unref(pxbclear);
      plww = 0;
   }

   if (mode == 4) return;

   *caption = *comments = 0;
   exif_get(ss_newfile,keynames,keyvals,2);                                      //  get captions and comments metadata

   if (keyvals[0]) {
      strncpy0(caption,keyvals[0],200);
      zfree(keyvals[0]);
   }

   if (keyvals[1]) {
      strncpy0(comments,keyvals[1],200);
      zfree(keyvals[1]);
   }

   *text = 0;

   if (mode == 1 && *caption) strcpy(text,caption);                              //  show caption only
   if (mode == 2 && *comments) strcpy(text,comments);                            //  show comments only
   if (mode == 3) {
      if (*caption) strcpy(text,caption);                                        //  show both on two lines
      if (*comments && *caption) strcat(text,"\n");
      if (*comments) strcat(text,comments);
   }

   if (! *text) return;

   for (int ii = 0; text[ii]; ii++)                                              //  replace "\n" with real newlines    15.01.1
      if (text[ii] == '\\' && text[ii+1] == 'n')
         memmove(text+ii,"\n ",2);

   pangofont = pango_font_description_from_string("Sans 12");                    //  make pango layout for font
   pangolayout = gtk_widget_create_pango_layout(Cdrawin,0);
   pango_layout_set_font_description(pangolayout,pangofont);
   pango_layout_set_text(pangolayout,text,-1);                                   //  add text to layout
   pango_layout_get_pixel_size(pangolayout,&plww,&plhh);

   ss_cr = gdk_cairo_create(gdkwin);

   cairo_set_line_width(ss_cr,1);
   cairo_set_source_rgb(ss_cr,1,1,1);                                            //  draw white background
   cairo_rectangle(ss_cr,10,10,plww,plhh);
   cairo_fill(ss_cr);

   cairo_move_to(ss_cr,10,10);                                                   //  draw layout with text
   cairo_set_source_rgb(ss_cr,0,0,0);
   pango_cairo_show_layout(ss_cr,pangolayout);

   cairo_destroy(ss_cr);
   return;
}


//  Load image and rescale to fit in window size.
//  If image aspect ratio is close enough to window ratio,
//  truncate to avoid having margins around around the image.

PIXBUF * ss_loadpxb(char *file)
{
   PIXBUF      *pxbin, *pxbtemp, *pxbout;
   GError      *gerror = 0;
   int         ww1, hh1, ww2, hh2;
   int         Iorgx, Iorgy, Worgx, Worgy;
   float       Rm, Rw, dR;
   
   Dww = gdk_window_get_width(gdkwin);                                           //  refresh drawing window size
   Dhh = gdk_window_get_height(gdkwin);                                          //  15.08

   pxbin = gdk_pixbuf_new_from_file(file,&gerror);                               //  load image file into pixbuf
   if (! pxbin) {
      zmessageACK(Mwin,"%s",gerror->message);
      return 0;
   }
   
   pxbtemp = gdk_pixbuf_stripalpha(pxbin);                                       //  stip alpha channel if present      15.08
   if (pxbtemp) {
      g_object_unref(pxbin);
      pxbin = pxbtemp;
   }

   ww1 = gdk_pixbuf_get_width(pxbin);                                            //  image dimensions
   hh1 = gdk_pixbuf_get_height(pxbin);

   ww2 = ss_ww;                                                                  //  window dimensions
   hh2 = ss_hh;
   
   Rm = 1.0 * ww1 / hh1;                                                         //  image width/height ratio
   Rw = 1.0 * ww2 / hh2;                                                         //  window width/height ratio
   dR = fabsf(Rm - Rw) / Rw;                                                     //  discrepancy ratio

   if (dR <= 0.01 * ss_cliplimit) {                                              //  discrepancy within user limit
      if (Rw >= Rm) {
         ww1 = ww2;                                                              //  height will be clipped
         hh1 = ww1 / Rm;
      }
      else {
         hh1 = hh2;                                                              //  width will be clipped
         ww1 = hh1 * Rm;
      }
   }
   else {                                                                        //  discrepancy too great
      if (Rw >= Rm) {
         hh1 = hh2;                                                              //  ratio image to fit in window
         ww1 = hh1 * Rm;
      }
      else {
         ww1 = ww2;
         hh1 = ww1 / Rm;
      }
   }

   pxbtemp = gdk_pixbuf_scale_simple(pxbin,ww1,hh1,BILINEAR);                    //  rescale image
   g_object_unref(pxbin);

   Iorgx = (ww1 - ww2) / 2.0;                                                    //  top left corner of image to copy from
   if (Iorgx < 0) Iorgx = 0;
   Iorgy = (hh1 - hh2) / 2.0;
   if (Iorgy < 0) Iorgy = 0;

   Worgx = (ww2 - ww1) / 2.0;                                                    //  top left corner of window to copy to
   if (Worgx < 0) Worgx = 0;
   Worgy = (hh2 - hh1) / 2.0;
   if (Worgy < 0) Worgy = 0;

   if (ww2 < ww1) ww1 = ww2;                                                     //  copy width
   if (hh2 < hh1) hh1 = hh2;                                                     //  copy height

   pxbout = gdk_pixbuf_new(GDKRGB,0,8,ww2,hh2);                                  //  output pixbuf = window size        15.01
   gdk_pixbuf_fill(pxbout,0);                                                    //  black margins
   gdk_pixbuf_copy_area(pxbtemp,Iorgx,Iorgy,ww1,hh1,pxbout,Worgx,Worgy);         //  insert image
   g_object_unref(pxbtemp);
   
   ss_rs = gdk_pixbuf_get_rowstride(pxbout);                                     //  set image row stride               15.12
   return pxbout;
}


//  write black to entire window

void ss_blankwindow()
{
   GdkRGBA     GDKdark;

   GDKdark.red = GDKdark.green = GDKdark.blue = 0.2;
   GDKdark.alpha = 1;
   ss_cr = gdk_cairo_create(gdkwin);
   gdk_cairo_set_source_rgba(ss_cr,&GDKdark);
   cairo_paint(ss_cr);
   cairo_destroy(ss_cr);
   zmainloop();                                                                  //  16.04
   return;
}


//  instant transition (also used for keyboard arrow keys)

void ss_instant()
{
   ss_cr = gdk_cairo_create(gdkwin);
   gdk_cairo_set_source_pixbuf(ss_cr,ss_pxbnew,0,0);
   cairo_paint(ss_cr);
   cairo_destroy(ss_cr);
   zmainloop();                                                                  //  16.04
   return;
}


//  fade-out / fade-in transition

void ss_fadein()
{
   PIXBUF      *pxbmix;
   int         ii, jj, kk, px, py, rs, iinc;
   float       newpart, oldpart;
   uint8       *pixels1, *pixels2, *pixels3;
   uint8       *pix1, *pix2, *pix3;

   pxbmix = gdk_pixbuf_copy(ss_pxbold);
   rs = gdk_pixbuf_get_rowstride(pxbmix);

   pixels1 = gdk_pixbuf_get_pixels(ss_pxbold);
   pixels2 = gdk_pixbuf_get_pixels(ss_pxbnew);
   pixels3 = gdk_pixbuf_get_pixels(pxbmix);

   iinc = 100.0 / (1 + ss_slowdown / 2.0);                                       //  slowdown factor                    15.07
   if (iinc < 1) iinc = 1;

   for (ii = 0; ii <= 1000; ii += iinc)
   {
      newpart = 0.001 * ii;
      oldpart = 1.0 - newpart;

      for (jj = 0; jj < 2; jj++)                                                 //  four passes, each modifies 25%
      for (kk = 0; kk < 2; kk++)                                                 //    of the pixels (visually smoother)
      {
         for (py = jj; py < ss_hh; py += 2)
         for (px = kk; px < ss_ww; px += 2)
         {
            pix1 = pixels1 + py * ss_rs + px * 3;
            pix2 = pixels2 + py * ss_rs + px * 3;
            pix3 = pixels3 + py * rs + px * 3;
            pix3[0] = newpart * pix2[0] + oldpart * pix1[0];
            pix3[1] = newpart * pix2[1] + oldpart * pix1[1];
            pix3[2] = newpart * pix2[2] + oldpart * pix1[2];
         }

         ss_cr = gdk_cairo_create(gdkwin);
         gdk_cairo_set_source_pixbuf(ss_cr,pxbmix,0,0);
         cairo_paint(ss_cr);
         cairo_destroy(ss_cr);
         zmainloop();                                                            //  16.04
      }
   }

   g_object_unref(pxbmix);
   return;
}


//  new image rolls over prior image from left to right

void ss_rollright()
{
   PIXBUF      *pixbuf;
   int         px;
   float       delay = 1.0 * ss_slowdown / ss_ww;
   uint8       *pixels, *pix3;

   pixels = gdk_pixbuf_get_pixels(ss_pxbnew);

   for (px = 0; px < ss_ww-4; px += 4)                                           //  4-wide
   {
      pix3 = pixels + px * 3;
      pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,4,ss_hh,ss_rs,0,0);
      ss_cr = gdk_cairo_create(gdkwin);
      gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,px,0);
      cairo_paint(ss_cr);
      cairo_destroy(ss_cr);
      g_object_unref(pixbuf);
      zmainloop();                                                               //  16.04
      zsleep(delay);
   }

   return;
}


//  new image rolls over prior image from top down

void ss_rolldown()
{
   PIXBUF      *pixbuf;
   int         py;
   float       delay = 1.0 * ss_slowdown / ss_hh;
   uint8       *pixels, *pix3;

   pixels = gdk_pixbuf_get_pixels(ss_pxbnew);

   for (py = 0; py < ss_hh-2; py += 4)                                           //  4-deep
   {
      pix3 = pixels + py * ss_rs;
      pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,ss_ww,4,ss_rs,0,0);
      ss_cr = gdk_cairo_create(gdkwin);
      gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,0,py);
      cairo_paint(ss_cr);
      cairo_destroy(ss_cr);
      g_object_unref(pixbuf);
      zmainloop();                                                               //  16.04
      zsleep(delay);
   }

   return;
}


//  new image opens up in horizontal rows like venetian blinds

void ss_venetian()
{
   PIXBUF      *pixbuf;
   int         py1, py2;
   uint8       *pixels, *pix3;
   int         louver, Nlouvers = 20;
   int         louversize = ss_hh / Nlouvers;
   float       delay = 1.0 / louversize * (1 + ss_slowdown / 8.0);

   pixels = gdk_pixbuf_get_pixels(ss_pxbnew);

   for (py1 = 0; py1 < louversize; py1++)                                        //  y-row within each louver
   {
      ss_cr = gdk_cairo_create(gdkwin);

      for (louver = 0; louver < Nlouvers; louver++)                              //  louver, first to last
      {
         py2 = py1 + louver * louversize;
         if (py2 >= ss_hh) break;
         pix3 = pixels + py2 * ss_rs;
         pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,ss_ww,1,ss_rs,0,0);
         gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,0,py2);
         cairo_paint(ss_cr);
         g_object_unref(pixbuf);
      }

      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04
      zsleep(delay);
   }

   return;
}


//  a grate opens up to show new image

void ss_grate()
{
   PIXBUF      *pixbuf;
   int         px1, px2, py1, py2;
   uint8       *pixels, *pix3;
   int         row, col, Nrow, Ncol;                                             //  rows and columns
   int         boxww, boxhh;
   float       delay;

   pixels = gdk_pixbuf_get_pixels(ss_pxbnew);

   Ncol = 20;                                                                    //  20 columns
   boxww = boxhh = ss_ww / Ncol;                                                 //  square boxes
   Nrow = ss_hh / boxhh;                                                         //  corresp. rows
   Ncol++;                                                                       //  round up
   Nrow++;
   delay = 1.0 / boxhh * (1 + ss_slowdown / 8.0);

   for (py1 = 0; py1 < boxhh; py1++)
   {
      ss_cr = gdk_cairo_create(gdkwin);

      for (row = 0; row < Nrow; row++)
      {
         py2 = py1 + row * boxhh;
         if (py2 >= ss_hh) break;
         pix3 = pixels + py2 * ss_rs;
         pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,ss_ww,1,ss_rs,0,0);
         gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,0,py2);
         cairo_paint(ss_cr);
         g_object_unref(pixbuf);
      }

      px1 = py1;

      for (col = 0; col < Ncol; col++)
      {
         px2 = px1 + col * boxww;
         if (px2 >= ss_ww) break;
         pix3 = pixels + px2 * 3;
         pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,1,ss_hh,ss_rs,0,0);
         gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,px2,0);
         cairo_paint(ss_cr);
         g_object_unref(pixbuf);
      }

      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04
      zsleep(delay);
   }

   return;
}


//  A rectangle opens up from the center and expands outward

void ss_rectangle()
{
   PIXBUF      *pixbuf;
   int         px1, py1, px2, py2, px3, py3;
   int         ww1, hh1, ww2, hh2;
   uint8       *pixels, *pix3;
   int         step, Nsteps = 200;
   float       delay = 1.0 / Nsteps * (1 + ss_slowdown / 8.0);

   pixels = gdk_pixbuf_get_pixels(ss_pxbnew);

   for (step = 1; step < Nsteps; step++)
   {
      ww1 = ss_ww * step / Nsteps;
      hh1 = ww1 * ss_hh / ss_ww;
      ww2 = ss_ww / Nsteps / 2 + 1;
      hh2 = ss_hh / Nsteps / 2 + 1;

      px1 = (ss_ww - ww1) / 2;
      py1 = (ss_hh - hh1) / 2;
      px2 = px1 + ww1 - ww2;
      py2 = py1;
      px3 = px1;
      py3 = py1 + hh1 - hh2;

      ss_cr = gdk_cairo_create(gdkwin);

      pix3 = pixels + py1 * ss_rs + px1 * 3;
      pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,ww1+1,hh2+1,ss_rs,0,0);
      gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,px1,py1);
      cairo_paint(ss_cr);
      g_object_unref(pixbuf);

      pix3 = pixels + py2 * ss_rs + px2 * 3;
      pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,ww2+1,hh1+1,ss_rs,0,0);
      gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,px2,py2);
      cairo_paint(ss_cr);
      g_object_unref(pixbuf);

      pix3 = pixels + py3 * ss_rs + px3 * 3;
      pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,ww1+1,hh2+1,ss_rs,0,0);
      gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,px3,py3);
      cairo_paint(ss_cr);
      g_object_unref(pixbuf);

      pix3 = pixels + py1 * ss_rs + px1 * 3;
      pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,ww2+1,hh1+1,ss_rs,0,0);
      gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,px1,py1);
      cairo_paint(ss_cr);
      g_object_unref(pixbuf);

      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04
      zsleep(delay);
   }

   ss_cr = gdk_cairo_create(gdkwin);
   gdk_cairo_set_source_pixbuf(ss_cr,ss_pxbnew,0,0);
   cairo_paint(ss_cr);
   cairo_destroy(ss_cr);

   return;
}


//  old image shrinks to the center, revealing new image

void ss_implode()                                                                //  15.03
{
   int         Nsteps = 100 + 50 * ss_slowdown;
   int         ww, hh, px, py;
   float       size = 1.0, F;
   PIXBUF      *pxbnew, *pxbold;

   F = 1.0 * (Nsteps-1) / Nsteps;

   while (true)
   {
      pxbnew = gdk_pixbuf_copy(ss_pxbnew);                                       //  new image at full size

      size = F * pow(size,1.02);
      if (size < 0.02) break;

      ww = ss_ww * size;
      hh = ss_hh * size;
      pxbold = pixbuf_rescale_fast(ss_pxbold,ww,hh);                             //  old image at reduced size

      px = ss_ww * 0.5 * (1.0 - size);                                           //  new image position, NW corner --> center
      py = ss_hh * 0.5 * (1.0 - size);
      gdk_pixbuf_copy_area(pxbold,0,0,ww,hh,pxbnew,px,py);                       //  copy shrinking old image into new image

      ss_cr = gdk_cairo_create(gdkwin);
      gdk_cairo_set_source_pixbuf(ss_cr,pxbnew,0,0);                             //  paint new image
      cairo_paint(ss_cr);
      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04

      g_object_unref(pxbnew);
      g_object_unref(pxbold);
   }

   ss_cr = gdk_cairo_create(gdkwin);
   gdk_cairo_set_source_pixbuf(ss_cr,ss_pxbnew,0,0);
   cairo_paint(ss_cr);
   cairo_destroy(ss_cr);

   return;
}


//  new image grows from the center, covering old image

void ss_explode()                                                                //  15.04
{
   int         Nsteps = 60 + 20 * ss_slowdown;
   int         ww, hh, px, py;
   float       size = 0.05, F;
   PIXBUF      *pxbnew, *pxbold;

   F = 1.0 * (Nsteps+1) / Nsteps;

   while (true)
   {
      pxbold = gdk_pixbuf_copy(ss_pxbold);                                       //  old image at full size

      size = F * pow(size,0.995);
      if (size > 1.0) break;

      ww = ss_ww * size;
      hh = ss_hh * size;
      pxbnew = pixbuf_rescale_fast(ss_pxbnew,ww,hh);                             //  new image at reduced size

      px = ss_ww * 0.5 * (1.0 - size);                                           //  new image position, center --> NW corner
      py = ss_hh * 0.5 * (1.0 - size);
      gdk_pixbuf_copy_area(pxbnew,0,0,ww,hh,pxbold,px,py);                       //  copy shrinking old image into new image

      ss_cr = gdk_cairo_create(gdkwin);
      gdk_cairo_set_source_pixbuf(ss_cr,pxbold,0,0);                             //  paint new image
      cairo_paint(ss_cr);
      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04

      g_object_unref(pxbnew);
      g_object_unref(pxbold);
   }

   ss_cr = gdk_cairo_create(gdkwin);
   gdk_cairo_set_source_pixbuf(ss_cr,ss_pxbnew,0,0);
   cairo_paint(ss_cr);
   cairo_destroy(ss_cr);

   return;
}


//  New image sweeps into view like a circular radar image

void ss_radar()                                                                  //  15.04
{
   int         px, py, ww, hh;
   int         px1, py1, px2, py2;
   int         ww2 = ss_ww/2, hh2 = ss_hh/2;
   float       R, Rmax, dR, T, dT;
   float       r, r1, r2;
   float       cosT, sinT;
   uint8       *pixels1, *pixels3, *pix1, *pix3;
   GdkPixbuf   *pixbuf;

   px = py = 0;                                                                  //  suppress compiler warnings

   pixels1 = gdk_pixbuf_get_pixels(ss_pxbold);
   pixels3 = gdk_pixbuf_get_pixels(ss_pxbnew);

   Rmax = sqrt(ww2*ww2 + hh2*hh2);                                               //  max. line length, center to corner
   dR = 100;                                                                     //  line segment length
   dT = 1.2 / Rmax;                                                              //  angle step                         16.04

   for (T = 0; T < 2*PI; T += dT)                                                //  angle from 0 to 360 deg.
   {
      cosT = cosf(T);
      sinT = sinf(T);

      ss_cr = gdk_cairo_create(gdkwin);

      for (R = 0; R < Rmax; R += dR)                                             //  R from center to edge
      {
         r1 = R;                                                                 //  R segment
         r2 = R + dR;

         px1 = ww2 + r1 * cosT;                                                  //  R segment
         px2 = ww2 + r2 * cosT;
         py1 = hh2 - r1 * sinT;
         py2 = hh2 - r2 * sinT;

         for (r = r1; r <= r2; r++)                                              //  loop R segment pixels
         {
            px = ww2 + r * cosT;
            py = hh2 - r * sinT;

            if (px < 0) px = 0;
            if (px > ss_ww-2) px = ss_ww-2;
            if (py < 0) py = 0;
            if (py > ss_hh-1) py = ss_hh-1;

            pix1 = pixels1 + py * ss_rs + px * 3;                                //  copy new image pixel to old image
            pix3 = pixels3 + py * ss_rs + px * 3;                                //  right side
            memcpy(pix1,pix3,6);

            if (px == 0 || px == ss_ww-2) break;                                 //  reached edge of image
            if (py == 0 || py == ss_hh-1) break;
         }

         px2 = px;                                                               //  actual end of R segment
         py2 = py;

         if (px1 < px2) px = px1;                                                //  get rectangle enclosing R segment
         else px = px2;
         if (py1 < py2) py = py1;
         else py = py2;
         ww = abs(px2-px1) + 1;
         hh = abs(py2-py1) + 1;
         if (ww <= 0) break;
         if (hh <= 0) break;

         if (px < 0) px = 0;
         if (px > ss_ww-1) px = ss_ww-1;
         if (px + ww > ss_ww) ww = ss_ww - px;

         if (py < 0) py = 0;
         if (py > ss_hh-1) py = ss_hh-1;
         if (py + hh > ss_hh) hh = ss_hh - py;

         pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbold,px,py,ww,hh);               //  paint window rectangle
         gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,px,py);
         cairo_paint(ss_cr);
         g_object_unref(pixbuf);
      }

      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04

      zsleep(0.00002 * ss_slowdown);
   }

   return;
}


//  New image spirals outwards from the center

void ss_spiral()                                                                 //  16.04
{
   int         px, py, ww, hh;
   int         px1, py1, px2, py2;
   int         ww2 = ss_ww/2, hh2 = ss_hh/2;
   float       R, Rmax, dR, T, dT;
   float       r, r1, r2;
   float       cosT, sinT;
   uint8       *pixels1, *pixels3, *pix1, *pix3;
   GdkPixbuf   *pixbuf;

   px = py = 0;                                                                  //  suppress compiler warnings

   pixels1 = gdk_pixbuf_get_pixels(ss_pxbold);
   pixels3 = gdk_pixbuf_get_pixels(ss_pxbnew);

   Rmax = sqrt(ww2*ww2 + hh2*hh2);                                               //  max. line length, center to corner
   dR = 140;                                                                     //  radius step
   dT = 1.2 - 0.1 * ss_slowdown;                                                 //  angle step
   if (dT < 0.2) dT = 0.2;
   dT = dT / Rmax;

   ss_cr = gdk_cairo_create(gdkwin);

   for (R = 0; R < Rmax; R += dR)                                                //  R from center to edge
   {
      for (T = 0; T < 2*PI; T += dT)                                             //  angle from 0 to 360 deg.
      {
         cosT = cosf(T);
         sinT = sinf(T);

         r1 = R;                                                                 //  R segment
         r2 = R + dR;

         px1 = ww2 + r1 * cosT;                                                  //  R segment
         px2 = ww2 + r2 * cosT;
         py1 = hh2 - r1 * sinT;
         py2 = hh2 - r2 * sinT;

         for (r = r1; r <= r2; r++)                                              //  loop R segment pixels
         {
            px = ww2 + r * cosT;
            py = hh2 - r * sinT;

            if (px < 0) px = 0;
            if (px > ss_ww-2) px = ss_ww-2;
            if (py < 0) py = 0;
            if (py > ss_hh-1) py = ss_hh-1;

            pix1 = pixels1 + py * ss_rs + px * 3;                                //  copy new image pixel to old image
            pix3 = pixels3 + py * ss_rs + px * 3;                                //  right side
            memcpy(pix1,pix3,6);

            if (px == 0 || px == ss_ww-2) break;                                 //  reached edge of image
            if (py == 0 || py == ss_hh-1) break;
         }

         px2 = px;                                                               //  actual end of R segment
         py2 = py;

         if (px1 < px2) px = px1;                                                //  get rectangle enclosing R segment
         else px = px2;
         if (py1 < py2) py = py1;
         else py = py2;
         ww = abs(px2-px1) + 1;
         hh = abs(py2-py1) + 1;
         if (ww <= 0) break;
         if (hh <= 0) break;

         if (px < 0) px = 0;
         if (px > ss_ww-1) px = ss_ww-1;
         if (px + ww > ss_ww) ww = ss_ww - px;

         if (py < 0) py = 0;
         if (py > ss_hh-1) py = ss_hh-1;
         if (py + hh > ss_hh) hh = ss_hh - py;

         pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbold,px,py,ww,hh);               //  paint window rectangle
         gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,px,py);
         cairo_paint(ss_cr);
         g_object_unref(pixbuf);
         zmainloop();
      }
   }

   cairo_destroy(ss_cr);
   return;
}


//  new image opens up like a Japanese fan

void ss_japfan()                                                                 //  15.04
{
   int         px, py, pxL, ww, hh;
   int         px1, py1, px2, py2;
   int         ww2 = ss_ww/2, hh2 = ss_hh/2;
   float       R, Rmax, dR, T, dT;
   float       r, r1, r2;
   float       cosT, sinT;
   uint8       *pixels1, *pixels3, *pix1, *pix3;
   GdkPixbuf   *pixbuf;

   px = py = 0;                                                                  //  suppress compiler warnings

   pixels1 = gdk_pixbuf_get_pixels(ss_pxbold);
   pixels3 = gdk_pixbuf_get_pixels(ss_pxbnew);

   Rmax = sqrt(ww2*ww2 + hh2*hh2);                                               //  max. line length, center to corner
   dR = 100;                                                                     //  line segment length
   dT = 1.2 / Rmax;                                                              //  angle step                         16.04

   for (T = PI/2; T > -PI/2; T -= dT)                                            //  angle from +90 to -90 deg.
   {
      cosT = cosf(T);
      sinT = sinf(T);

      ss_cr = gdk_cairo_create(gdkwin);

      for (R = 0; R < Rmax; R += dR)                                             //  R from center to edge
      {
         r1 = R;                                                                 //  R segment
         r2 = R + dR;

         px1 = ww2 + r1 * cosT;                                                  //  R segment
         px2 = ww2 + r2 * cosT;
         py1 = hh2 - r1 * sinT;
         py2 = hh2 - r2 * sinT;

         for (r = r1; r <= r2; r++)                                              //  loop R segment pixels
         {
            px = ww2 + r * cosT;
            py = hh2 - r * sinT;

            if (px > ss_ww-2) px = ss_ww-2;
            if (py < 0) py = 0;
            if (py > ss_hh-1) py = ss_hh-1;

            pix1 = pixels1 + py * ss_rs + px * 3;                                //  copy new image pixel to old image
            pix3 = pixels3 + py * ss_rs + px * 3;                                //  right side
            memcpy(pix1,pix3,6); 

            pxL = ss_ww-1 - px;

            pix1 = pixels1 + py * ss_rs + pxL * 3;                               //  left side pixel
            pix3 = pixels3 + py * ss_rs + pxL * 3;
            memcpy(pix1,pix3,6);

            if (pxL == 0) break;                                                 //  reached edge of image
            if (py == 0 || py == ss_hh-1) break;
         }

         px2 = px;                                                               //  actual end of R segment
         py2 = py;

         px = px1;
         if (py1 < py2) py = py1;                                                //  get rectangle enclosing R segment
         else py = py2;
         ww = px2 - px1 + 1;
         hh = abs(py2-py1) + 1;
         if (ww <= 0) break;
         if (hh <= 0) break;

         if (px < 0) px = 0;
         if (px > ss_ww-1) px = ss_ww-1;
         if (px + ww > ss_ww) ww = ss_ww - px;

         if (py < 0) py = 0;
         if (py > ss_hh-1) py = ss_hh-1;
         if (py + hh > ss_hh) hh = ss_hh - py;

         pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbold,px,py,ww,hh);               //  paint window rectangle
         gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,px,py);
         cairo_paint(ss_cr);
         g_object_unref(pixbuf);

         pxL = ss_ww-1 - px2;

         pixbuf = gdk_pixbuf_new_subpixbuf(ss_pxbold,pxL,py,ww,hh);              //  left side rectangle
         gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,pxL,py);
         cairo_paint(ss_cr);
         g_object_unref(pixbuf);
      }

      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04

      zsleep(0.00002 * ss_slowdown);
   }

   return;
}


//  New image closes in from top and bottom with jagged teeth

void ss_jaws()
{
   PIXBUF      *pixbuf;
   int         nteeth = 20, Np = 10;
   int         tbase1, tbase2, twidth, tlength, tooth, tpos;
   int         ii, px, py, ww, ww2;
   float       delay = 0.0005 * ss_slowdown;
   uint8       *pixels, *pix3;

   ss_cr = gdk_cairo_create(gdkwin);
   pixels = gdk_pixbuf_get_pixels(ss_pxbnew);

   twidth = ss_ww / nteeth;
   tlength = twidth;

   Np = Np / (1.0 + ss_slowdown / 4.0);
   if (Np < 1) Np = 1;

   for (ii = 0; ii <= ss_hh/2 - tlength/2 + 1; ii += Np)
   {
      tbase1 = ii;                                                               //  tooth base from window top to middle
      tbase2 = ss_hh - tbase1 - 1;                                               //  tooth base from window bottom to middle

      for (tooth = 0; tooth <= nteeth; tooth++)                                  //  tooth first to last + 1
      {
         for (tpos = 0; tpos < tlength; tpos += Np)                              //  tooth position from base to point
         {
            ww = twidth * (tlength - tpos) / tlength;                            //  tooth width at scan line
            if (ww < 2) break;

            py = tbase1 + tpos;                                                  //  top teeth scan line y
            px = twidth / 2 + tooth * twidth - ww / 2;                           //  scan line x to x + ww
            if (px < ss_ww) {
               ww2 = ww;
               if (px + ww2 > ss_ww) ww2 = ss_ww - px;
               pix3 = pixels + py * ss_rs + px * 3;
               pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,ww2,Np,ss_rs,0,0);
               gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,px,py);
               cairo_paint(ss_cr);
               g_object_unref(pixbuf);
            }

            py = tbase2 - tpos;                                                  //  bottom teeth scan line y
            py = py - Np;
            px = tooth * twidth - ww / 2;                                        //  scan line x to x + ww
            if (tooth == 0) {
               px = 0;                                                           //  leftmost tooth is half
               ww = ww / 2;
            }
            if (px < ss_ww) {
               ww2 = ww;
               if (px + ww2 > ss_ww) ww2 = ss_ww - px;
               pix3 = pixels + py * ss_rs + px * 3;
               pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,ww2,Np,ss_rs,0,0);
               gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,px,py);
               cairo_paint(ss_cr);
               g_object_unref(pixbuf);
            }
         }
      }
      zsleep(delay);                                                             //  15.04
      zmainloop();                                                               //  16.04
   }

   gdk_cairo_set_source_pixbuf(ss_cr,ss_pxbnew,0,0);
   cairo_paint(ss_cr);
   cairo_destroy(ss_cr);
   return;
}


//  An ellipse opens up from the center and expands outward

void ss_ellipse()
{
   PIXBUF      *pixbuf;
   uint8       *pixels, *pix3;
   int         step, Nsteps = 100;
   int         px1, py1, ww;
   float       delay = 0.1 / Nsteps;
   float       a, b, a2, b2, px, py, px2, py2;
   float       ww2 = ss_ww / 2, hh2 = ss_hh / 2;

   delay = delay * (1 + ss_slowdown / 2.0);

   pixels = gdk_pixbuf_get_pixels(ss_pxbnew);

   for (step = 1; step < 1.4 * Nsteps; step++)
   {
      a = ww2 * step / Nsteps;                                                   //  ellipse a and b constants
      b = a * ss_hh / ss_ww;                                                     //    from tiny to >> image size
      a2 = a * a;
      b2 = b * b;

      ss_cr = gdk_cairo_create(gdkwin);

      for (py = -b; py <= +b; py += 3)                                           //  py from top of ellipse to bottom
      {
         while (py < -(hh2-2)) py += 3;
         if (py > hh2-2) break;
         py2 = py * py;
         px2 = a2 * (1.0 - py2 / b2);                                            //  corresponding px value,
         px = sqrt(px2);                                                         //  (+/- from center of ellipse)
         if (px > ww2) px = ww2;
         ww = 2 * px;                                                            //  length of line thru ellipse
         if (ww < 2) continue;
         px1 = ww2 - px;                                                         //  relocate origin
         py1 = py + hh2;
         if (px1 + ww > ss_ww) px1 = ss_ww - ww;                                 //  insurance
         if (py1 + 3 > ss_hh) py1 = ss_hh - 3;
         pix3 = pixels + py1 * ss_rs + px1 * 3;
         pixbuf = gdk_pixbuf_new_from_data(pix3,GDKRGB,0,8,ww,3,ss_rs,0,0);
         gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,px1,py1);
         cairo_paint(ss_cr);
         g_object_unref(pixbuf);
      }

      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04
      zsleep(delay);
   }

   ss_cr = gdk_cairo_create(gdkwin);
   gdk_cairo_set_source_pixbuf(ss_cr,ss_pxbnew,0,0);
   cairo_paint(ss_cr);
   cairo_destroy(ss_cr);

   return;
}


//  new image splats onto old image one drop at a time

void ss_raindrops()
{
   PIXBUF      *pxbmix, *pxbdrop;
   int         rsmix;
   int         px, py, px1, py1, px2, py2, cx, cy;
   int         Rmin, Rmax, R, R2, dist2, Ndrops;
   uint8       *pixels2, *pixels3;
   uint8       *pix2, *pix3 = 0;
   float       dtime;

   pixels2 = gdk_pixbuf_get_pixels(ss_pxbnew);                                   //  source image

   pxbmix = gdk_pixbuf_new(GDKRGB,0,8,ss_ww,ss_hh);                              //  destination image
   pixels3 = gdk_pixbuf_get_pixels(pxbmix);
   rsmix = gdk_pixbuf_get_rowstride(pxbmix);
   memset(pixels3,0,ss_hh * rsmix);                                              //  clear dest. to black

   ss_cr = gdk_cairo_create(gdkwin);
   gdk_cairo_set_source_pixbuf(ss_cr,pxbmix,0,0);                                //  paint black window
   cairo_paint(ss_cr);
   cairo_destroy(ss_cr);

   Rmin = ss_ww * 0.01;                                                          //  drop size range
   Rmax = ss_ww * 0.02;
   Ndrops = 3000;

   for (int ii = 0; ii < Ndrops; ii++)
   {
      cx = drandz() * ss_ww;                                                     //  drop location on image
      cy = drandz() * ss_hh;
      R = drandz() * Rmax + Rmin;                                                //  drop size
      R2 = R * R;
      px1 = cx - R;
      if (px1 < 0) px1 = 0;
      py1 = cy - R;
      if (py1 < 0) py1 = 0;
      px2 = cx + R;
      if (px2 >= ss_ww) px2 = ss_ww;
      py2 = cy + R;
      if (py2 > ss_hh) py2 = ss_hh;

      for (py = py1; py < py2; py++)                                             //  copy drop area from new image
      for (px = px1; px < px2; px++)                                             //    to old image
      {
         dist2 = (px-cx) * (px-cx) + (py-cy) * (py-cy);
         if (dist2 > R2) continue;
         pix2 = pixels2 + py * ss_rs + px * 3;
         pix3 = pixels3 + py * rsmix + px * 3;
         memcpy(pix3,pix2,3);
      }

      pxbdrop = gdk_pixbuf_new_subpixbuf(pxbmix,px1,py1,px2-px1,py2-py1);
      ss_cr = gdk_cairo_create(gdkwin);
      gdk_cairo_set_source_pixbuf(ss_cr,pxbdrop,px1,py1);
      cairo_paint(ss_cr);
      cairo_destroy(ss_cr);

      g_object_unref(pxbdrop);
      dtime = 0.001 * (1.0 - pow(1.0*ii/Ndrops,0.1));
      dtime = dtime * (1.0 + 0.5 * ss_slowdown);
      zsleep(dtime);
      zmainloop();                                                               //  16.04
   }

   ss_cr = gdk_cairo_create(gdkwin);
   gdk_cairo_set_source_pixbuf(ss_cr,ss_pxbnew,0,0);                             //  fill bits that are still missing
   cairo_paint(ss_cr);
   cairo_destroy(ss_cr);
   g_object_unref(pxbmix);
   return;
}


//  new image spreads from the middle to left and right edges
//  like a double-door swinging open

void ss_doubledoor()
{
   #define GPNFD(pix,ww,hh) gdk_pixbuf_new_from_data(pix,GDKRGB,0,8,ww,hh,ss_rs,0,0)

   PIXBUF      *pixbuf;
   int         bx, px;
   uint8       *pixels, *pix3;
   float       delay = 0.2 / ss_ww;

   delay = delay * (1 + ss_slowdown);

   pixels = gdk_pixbuf_get_pixels(ss_pxbnew);

   for (bx = 0; bx < ss_ww/2; bx++)                                              //  bx = 0 ... ww/2
   {
      ss_cr = gdk_cairo_create(gdkwin);

      px = ss_ww / 2 - bx;
      pix3 = pixels + 3 * px;                                                    //  line from (ww/2-bx,0) to (ww/2-bx,hh-1)
      pixbuf = GPNFD(pix3,1,ss_hh);
      gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,px,0);
      cairo_paint(ss_cr);
      g_object_unref(pixbuf);

      px = ss_ww / 2 + bx;
      pix3 = pixels + 3 * px;                                                    //  line from (ww/2+bx,0) to (ww/2+bx,hh-1)
      pixbuf = GPNFD(pix3,1,ss_hh);
      gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,px,0);
      cairo_paint(ss_cr);
      g_object_unref(pixbuf);

      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04
      zsleep(delay);
   }

   return;
}


//  Rotate from old image to new image.

namespace ss_rotate_names
{
   int      cx1, cy1, cx2, cy2, cy3, cy4;
   uint8    *pixels1, *pixels2, *pixels3;
   int      rsmix, tbusy[max_threads];
}

void ss_rotate()
{
   using namespace ss_rotate_names;

   void * ss_rotate_thread1(void *arg);
   void * ss_rotate_thread2(void *arg);

   PIXBUF      *pxbmix;
   float       R, step, stepx = 1, Nsteps = 60;                                  //  16.04
   int         ii;

   pixels1 = gdk_pixbuf_get_pixels(ss_pxbold);                                   //  source images
   pixels2 = gdk_pixbuf_get_pixels(ss_pxbnew);

   pxbmix = gdk_pixbuf_new(GDKRGB,0,8,ss_ww,ss_hh);                              //  destination image
   rsmix = gdk_pixbuf_get_rowstride(pxbmix);
   pixels3 = gdk_pixbuf_get_pixels(pxbmix);

   Nsteps = Nsteps * (1.0 + ss_slowdown / 2.0);

   for (step = 0; step < Nsteps; step += stepx)
   {
      R = 1.0 * step / Nsteps;
      stepx = 2 - R;                                                             //  2 ... 1

      if (step + stepx >= Nsteps) {
         step = Nsteps;
         R = 1.0;
      }

      cx1 = R * ss_ww / 2.0;                                                     //  corners of shrinking trapezoid
      cy1 = 0.3 * R * ss_hh;
      cx2 = ss_ww - cx1;
      cy2 = 0;
      cy3 = ss_hh;
      cy4 = ss_hh - cy1;

      memset(pixels3,0,ss_hh * rsmix);

      for (ii = 0; ii < ss_nwt; ii++) {                                          //  start worker threads
         tbusy[ii] = 1;
         start_detached_thread(ss_rotate_thread1,&Nval[ii]);
      }

      for (ii = 0; ii < ss_nwt; ii++)
         while(tbusy[ii]) zsleep(0.001);

      ss_cr = gdk_cairo_create(gdkwin);                                          //  create and destroy each paint      15.02
      gdk_cairo_set_source_pixbuf(ss_cr,pxbmix,0,0);
      cairo_paint(ss_cr);
      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04
   }

   for (step = 0; step < Nsteps; step += stepx)
   {
      R = 1.0 * step / Nsteps;
      stepx = 1 + R;                                                             //  1 ... 2

      if (step + stepx >= Nsteps) {
         step = Nsteps;
         R = 1.0;
      }

      cx1 = ss_ww * (0.5 + 0.5 * R);                                             //  corners of expanding trapezoid
      cy1 = ss_hh * (0.3 - 0.3 * R);
      cx2 = ss_ww * (0.5 - 0.5 * R);
      cy2 = 0;
      cy3 = ss_hh;
      cy4 = ss_hh - cy1;

      memset(pixels3,0,ss_hh * rsmix);

      for (ii = 0; ii < ss_nwt; ii++) {                                          //  start worker threads
         tbusy[ii] = 1;
         start_detached_thread(ss_rotate_thread2,&Nval[ii]);
      }

      for (ii = 0; ii < ss_nwt; ii++)
         while(tbusy[ii]) zsleep(0.001);

      ss_cr = gdk_cairo_create(gdkwin);
      gdk_cairo_set_source_pixbuf(ss_cr,pxbmix,0,0);
      cairo_paint(ss_cr);
      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04
   }

   g_object_unref(pxbmix);
   return;
}

void * ss_rotate_thread1(void *arg)
{
   using namespace ss_rotate_names;

   int      index = *((int *) (arg));
   int      px, py, ylo, yhi, vpx, vpy;
   uint8    *pix1, *pix3;
   float    Rx, Ry;

   for (px = cx1 + index; px < cx2; px += ss_nwt)
   {
      Rx = 1.0 * (px - cx1) / (cx2 - cx1);
      ylo = cy1 + Rx * (cy2 - cy1);
      yhi = cy4 + Rx * (cy3 - cy4);

      for (py = ylo; py < yhi; py++)
      {
         Ry = 1.0 * (py - ylo) / (yhi - ylo);
         vpx = Rx * (ss_ww - 1);
         vpy = Ry * (ss_hh - 1);

         pix1 = pixels1 + vpy * ss_rs + vpx * 3;
         pix3 = pixels3 + py * rsmix + px * 3;
         memcpy(pix3,pix1,3);
      }
   }

   tbusy[index] = 0;
   pthread_exit(0);
   return 0;
}

void * ss_rotate_thread2(void *arg)
{
   using namespace ss_rotate_names;

   int      index = *((int *) (arg));
   int      px, py, ylo, yhi, vpx, vpy;
   uint8    *pix2, *pix3;
   float    Rx, Ry;

   for (px = cx2 + index; px < cx1; px += ss_nwt)
   {
      Rx = 1.0 * (px - cx2) / (cx1 - cx2);
      ylo = cy2 + Rx * (cy1 - cy2);
      yhi = cy3 + Rx * (cy4 - cy3);

      for (py = ylo; py < yhi; py++)
      {
         Ry = 1.0 * (py - ylo) / (yhi - ylo);
         vpx = Rx * (ss_ww - 1);
         vpy = Ry * (ss_hh - 1);

         pix2 = pixels2 + vpy * ss_rs + vpx * 3;
         pix3 = pixels3 + py * rsmix + px * 3;
         memcpy(pix3,pix2,3);
      }
   }

   tbusy[index] = 0;
   pthread_exit(0);
   return 0;
}


//  Old image falls over to reveal new image.

namespace ss_fallover_names
{
   int      cx1, cy1, cx2, cy2;
   uint8    *pixels1, *pixels2, *pixels3;
   int      rsmix, tbusy[max_threads];
}

void ss_fallover()                                                               //  15.06
{
   using namespace ss_fallover_names;

   void * ss_fallover_thread(void *arg);

   PIXBUF      *pxbmix;
   float       R, step, stepx = 1, Nsteps = 300;
   int         ii;

   pixels1 = gdk_pixbuf_get_pixels(ss_pxbold);                                   //  old image
   pixels2 = gdk_pixbuf_get_pixels(ss_pxbnew);                                   //  new image

   pxbmix = gdk_pixbuf_new(GDKRGB,0,8,ss_ww,ss_hh);                              //  output image - mixture
   rsmix = gdk_pixbuf_get_rowstride(pxbmix);
   pixels3 = gdk_pixbuf_get_pixels(pxbmix);

   Nsteps = Nsteps + 30 * ss_slowdown;

   for (step = 0; step < Nsteps; step += stepx)
   {
      stepx += 30.0 / Nsteps;                                                    //  acceleration
      
      R = 1.0 * step / Nsteps;                                                   //  0 ... 1
      if (step + stepx >= Nsteps) R = 1;                                         //  last iteration

      cx1 = 0.2 * ss_ww * R;                                                     //  top corners of falling old image
      cy1 = ss_hh * R;
      cx2 = ss_ww - cx1;
      cy2 = cy1;
      
      for (ii = 0; ii < ss_nwt; ii++) {                                          //  start worker threads
         tbusy[ii] = 1;                                                          //  make combined image
         start_detached_thread(ss_fallover_thread,&Nval[ii]);
      }

      for (ii = 0; ii < ss_nwt; ii++)                                            //  wait for completion
         while(tbusy[ii]) zsleep(0.01);

      ss_cr = gdk_cairo_create(gdkwin);                                          //  create and destroy each paint
      gdk_cairo_set_source_pixbuf(ss_cr,pxbmix,0,0);
      cairo_paint(ss_cr);
      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04
   }

   g_object_unref(pxbmix);
   return;
}

void * ss_fallover_thread(void *arg)                                             //  construct combined image
{
   using namespace ss_fallover_names;

   int      index = *((int *) (arg));
   int      px, py, npix, vpx, vpy;
   int      cx1a, cx2a;
   float    R;
   uint8    *pix1, *pix3;
   
   for (py = index; py < ss_hh; py += ss_nwt)                                    //  py = 0 ... ss_hh
   {
      px = 0;                                                                    //  px = 0 ... ss_ww, new image
      npix = ss_ww;
      pix1 = pixels2 + py * ss_rs + px * 3;
      pix3 = pixels3 + py * rsmix + px * 3;
      memcpy(pix3,pix1,npix*3);
      
      if (py < cy1) continue;

      R = 1.0 * (py - cy1) / (ss_hh - cy1);                                      //  0 ... 1
      cx1a = cx1 * (1.0 - R);                                                    //  cx1a = cx1 ... 0
      cx2a = cx2 + R * (ss_ww - cx2);                                            //  cx2a = cx2 ... ss_ww

      for (px = cx1a; px < cx2a; px++)                                           //  px = cx1a ... cx2a
      {
         vpx = ss_ww * (px - cx1a) / (cx2a - cx1a);                              //  vpx = 0 ... ss_ww
         vpy = ss_hh * R;                                                        //  vpy = 0 ... ss_hh
         pix1 = pixels1 + vpy * ss_rs + vpx * 3;
         pix3 = pixels3 + py * rsmix + px * 3;
         memcpy(pix3,pix1,3);
      }
   }
   
   tbusy[index] = 0;
   pthread_exit(0);
   return 0;
}


//  old image deforms from flat to sphere and then shrinks to reveal new image

namespace ss_spheroid_names
{
   float       Cx, Cy, D, R, F;
   PIXBUF      *pxbold, *pxbnew;
   uint8       *pixels1, *pixels3;
   int         tbusy[max_threads];
   float       *s1mem, *s2mem, Rmax;
}


void ss_spheroid()                                                               //  15.11
{
   using namespace ss_spheroid_names;

   void * ss_spheroid_thread(void *arg);
   
   int      ii, cc, px, py, dx, dy;

   float  Ftab1[14] = { 0.10, 0.15, 0.23, 0.34, 0.51, 0.76, 1.14,                //  D/ss_ww ratio                      15.12
                        1.71, 2.56, 3.84, 5.77, 8.65, 13.0, 19.5 };
   float  Ftab2[14] = { 0.90, 0.93, 0.95, 0.97, 0.97, 0.97, 0.97,                //  D reduction factor
                        0.95, 0.93, 0.90, 0.84, 0.76, 0.66, 0.50 };

   cc = ss_ww * ss_hh * sizeof(float);                                           //  15.12
   s1mem = (float *) zmalloc(cc);

   Rmax = 1.0 + 0.5 * sqrtf(ss_ww * ss_ww + ss_hh * ss_hh);
   cc = 10.1 * Rmax * sizeof(float);                                             //  bugfix: float rounding             16.06
   s2mem = (float *) zmalloc(cc);  

   pxbold = gdk_pixbuf_copy(ss_pxbold);                                          //  old image at full size
   pixels1 = gdk_pixbuf_get_pixels(pxbold);

   Cx = ss_ww / 2;                                                               //  center of image
   Cy = ss_hh / 2;

   for (py = 0; py < ss_hh; py++)                                                //  pre-calculate                      15.12
   for (px = 0; px < ss_ww; px++)
   {
      dx = px - Cx;
      dy = py - Cy;
      ii = py * ss_ww + px;
      s1mem[ii] = sqrtf(dx*dx + dy*dy);                                          //  dist. from center to pixel
   }

   F = 1.0;

   for (D = 20 * ss_ww; D > 10; D *= F)                                          //  loop from flat to sphere projection
   {
      R = D / ss_ww;
      for (ii = 1; ii < 13; ii++)                                                //  get F value for D / ss_ww ratio
         if (R < Ftab1[ii]) break;
      R = (R - Ftab1[ii-1]) / (Ftab1[ii] - Ftab1[ii-1]);
      F = Ftab2[ii-1] + R * (Ftab2[ii] - Ftab2[ii-1]);
      
      R = 0.02 * ss_slowdown;                                                    //  0.0 ... 0.99
      if (R > 1.0) R = 1.0;
      R = sqrtf(R);                                                              //  steepen curve at low end
      F = R * 0.99 + (1.0 - R) * F;                                              //  add slowdown factor

      pxbnew = gdk_pixbuf_copy(ss_pxbnew);                                       //  new image at full size
      pixels3 = gdk_pixbuf_get_pixels(pxbnew);

      for (int ii = 0; ii < ss_nwt; ii++) {                                      //  start worker threads
         tbusy[ii] = 1;                                                          //  make combined image
         start_detached_thread(ss_spheroid_thread,&Nval[ii]);
      }

      for (int ii = 0; ii < ss_nwt; ii++)                                        //  wait for completion
         while(tbusy[ii]) zsleep(0.001);

      ss_cr = gdk_cairo_create(gdkwin);
      gdk_cairo_set_source_pixbuf(ss_cr,pxbnew,0,0);                             //  paint new image
      cairo_paint(ss_cr);
      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04
      g_object_unref(pxbnew);
   }
   
   g_object_unref(pxbold);

   ss_cr = gdk_cairo_create(gdkwin);
   gdk_cairo_set_source_pixbuf(ss_cr,ss_pxbnew,0,0);
   cairo_paint(ss_cr);
   cairo_destroy(ss_cr);
   
   zfree(s1mem);                                                                 //  15.12
   zfree(s2mem);
   return;
}


void * ss_spheroid_thread(void *arg)
{
   using namespace ss_spheroid_names;

   int      index = *((int *) arg);
   int      ii, px0, py0, px3, py3, dx, dy;
   float    px1, py1, s1, s2, T;
   uint8    *pix0, *pix1, *pix2, *pix3, *pixx;
   float    f0, f1, f2, f3;
   
   for (ii = 0; ii < 10 * Rmax; ii++)                                            //  pre-calculate                      15.12
   {
      s1 = 0.1 * ii;
      T = s1 * PI / D;
      if (s1 == 0) s2mem[ii] = 0;
      else if (T > 1.0) s2mem[ii] = -1;
      else s2mem[ii] = D / PI * asinf(T) / s1;
   }

   for (py3 = index; py3 < ss_hh; py3 += ss_nwt)                                 //  loop output pixels
   for (px3 = 0; px3 < ss_ww; px3++)
   {
/***
      dx = px3 - Cx;                                                             //  code without pre-calculations
      dy = py3 - Cy;
      s1 = sqrtf(dx*dx + dy*dy);                                                 //  dist. from center to output pixel
      if (s1 == 0) continue;
      T = s1 * PI / D;                                                           //  sine of subtended angle
      if (T > 1.0) continue;
      s2 = D / PI * asinf(T);                                                    //  corresp. dist. on sphere
      px1 = Cx + dx * s2 / s1;                                                   //  input v.pixel
      py1 = Cy + dy * s2 / s1;
***/
      dx = px3 - Cx;
      dy = py3 - Cy;
      ii = py3 * ss_ww + px3;
      s1 = s1mem[ii];                                                            //  dist. from center to output pixel
      ii = 10 * s1;
      s2 = s2mem[ii];                                                            //  corresp. dist. on sphere / s1
      if (s2 < 0) continue;
      px1 = Cx + dx * s2;                                                        //  input v.pixel
      py1 = Cy + dy * s2;

      //  inline vpixel() for speed
      px0 = px1;                                                                 //  px0/py0: integer px1/py1
      py0 = py1;

      if (px0 < 0 || py0 < 0) continue;
      if (px0 > ss_ww-2 || py0 > ss_hh-2) continue;

      f0 = (px0+1 - px1) * (py0+1 - py1);                                        //  overlap of (px,py)
      f1 = (px0+1 - px1) * (py1 - py0);                                          //   in each of the 4 pixels
      f2 = (px1 - px0) * (py0+1 - py1);
      f3 = (px1 - px0) * (py1 - py0);

      pix0 = pixels1 + py0 * ss_rs + px0 * 3;                                    //  pixel (px0,py0)
      pix1 = pix0 + ss_rs;                                                       //        (px0,py0+1)
      pix2 = pix0 + 3;                                                           //        (px0+1,py0)
      pix3 = pix1 + 3;                                                           //        (px0+1,py0+1)

      pixx = pixels3 + py3 * ss_rs + px3 * 3;                                    //  input v.pixel >> output pixel
      pixx[0] = f0 * pix0[0] + f1 * pix1[0] + f2 * pix2[0] + f3 * pix3[0];
      pixx[1] = f0 * pix0[1] + f1 * pix1[1] + f2 * pix2[1] + f3 * pix3[1];
      pixx[2] = f0 * pix0[2] + f1 * pix1[2] + f2 * pix2[2] + f3 * pix3[2];
   }

   tbusy[index] = 0;
   pthread_exit(0);
   return 0;
}


//  new image turns up from lower right corner, like a book page

void ss_turnpage()                                                               //  15.12
{
   PIXBUF   *pixbuf3;
   uint8    *pixels1, *pixels2, *pixels3;
   uint8    *pix1, *pix2, *pix3;
   int      pxA, pxS;
   int      px1, py1, px3, py3;
   int      cc, np1, np2, f1;
   float    C;
   
   np1 = 30 - ss_slowdown;
   if (np1 < 1) np1 = 1;
   np2 = 2 + np1 / 5;

   pixels1 = gdk_pixbuf_get_pixels(ss_pxbold);                                   //  old image
   pixels2 = gdk_pixbuf_get_pixels(ss_pxbnew);                                   //  new image
   pixbuf3 = gdk_pixbuf_copy(ss_pxbold);                                         //  output image - mixture
   pixels3 = gdk_pixbuf_get_pixels(pixbuf3);                                     //  (initially = old image)
   
   for (pxA = ss_ww-1; ; pxA -= np1)                                             //  point A moves from lower right corner
   {                                                                             //    to the left
      for (px1 = pxA, py1 = ss_hh-1; px1 < ss_ww && py1 >= 0; px1++, py1--)
      {
         pix2 = pixels2 + py1 * ss_rs + px1 * 3;                                 //  new image row
         pix3 = pixels3 + py1 * ss_rs + px1 * 3;                                 //  output image row
         cc = 3 * (ss_ww - px1);
         if (px1 < 0) {
            pix2 -= 3 * px1;
            pix3 -= 3* px1;
            cc += 3 * px1;
         } 
         if (cc < 1) continue;
         if (cc > 3 * ss_ww) cc = cc / 2;
         memcpy(pix3,pix2,cc);                                                   //  paint new image from px1 to right edge
      }

      f1 = 1;         

      for (pxS = pxA; pxS < ss_ww-np2; pxS += np2)                               //  point S moves from point A to the right
      {
         C = 0.53 * (pxS - pxA) / (ss_ww - pxA);
         C = C * (pxS - pxA);

         for (px1 = pxS, py1 = ss_hh-1; px1 < ss_ww && py1 >= 0; px1++, py1--) 
         {         
            px3 = px1 - C;                                                       //  dest pixel = source pixel
            py3 = py1 - C;                                                       //    offset in NW direction
            if (px3 < 0 || px3 > ss_ww-1) continue;
            if (py3 < 0 || py3 > ss_hh-1) continue;
            pix1 = pixels1 + py1 * ss_rs + px1 * 3;                              //  source pixel --> dest pixel
            pix3 = pixels3 + py3 * ss_rs + px3 * 3;
            memcpy(pix3,pix1,3*np2);
            f1 = 0;
         }
      }
      
      ss_cr = gdk_cairo_create(gdkwin);
      gdk_cairo_set_source_pixbuf(ss_cr,pixbuf3,0,0);                            //  paint image
      cairo_paint(ss_cr);
      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04
      if (pxA < 0 && f1) break;
   }

   g_object_unref(pixbuf3);

   ss_cr = gdk_cairo_create(gdkwin);
   gdk_cairo_set_source_pixbuf(ss_cr,ss_pxbnew,0,0);
   cairo_paint(ss_cr);
   cairo_destroy(ss_cr);
   return;
}


//  French door: old image swings away left and right to reveal new image.

namespace ss_frenchdoor_names
{
   PIXBUF   *ss_pxbmix, *pxbmod;                                                 //  output image
   float    step, Nsteps, R, ww2;
   uint8    *pixels1, *pixels3;
   int      tbusy[max_threads];
   int      ww3, ww4, sww;
}


void ss_frenchdoor()                                                             //  16.04
{
   using namespace ss_frenchdoor_names;

   #define GPNFD(pix,ww,hh)   \
   gdk_pixbuf_new_from_data(pix,GDKRGB,0,8,ww,hh,ss_rs,0,0)
   
   void * ss_frenchdoor_thread(void *arg);

   Nsteps = 100 + 10 * ss_slowdown;
   ww2 = 0.5 * ss_ww;

   for (step = 0; step < Nsteps; step++)
   {
      R = step / Nsteps;

      ss_pxbmix = gdk_pixbuf_copy(ss_pxbnew);
      pixels1 = gdk_pixbuf_get_pixels(ss_pxbold);                                //  old image
      pixels3 = gdk_pixbuf_get_pixels(ss_pxbmix);

      for (int ii = 0; ii < ss_nwt; ii++) {                                      //  start worker threads
         tbusy[ii] = 1;                                                          //  make combined image
         start_detached_thread(ss_frenchdoor_thread,&Nval[ii]);
      }

      for (int ii = 0; ii < ss_nwt; ii++)                                        //  wait for completion
         while(tbusy[ii]) { zmainloop(10); }

      ss_cr = gdk_cairo_create(gdkwin);

      sww = ww2 / Nsteps + 6;                                                    //  left side
      ww3 = (1 - R) * ww2 + sww;
      pxbmod = GPNFD(pixels3,ww3,ss_hh);
      gdk_cairo_set_source_pixbuf(ss_cr,pxbmod,0,0);
      cairo_paint(ss_cr);
      g_object_unref(pxbmod);

      ww4 = (1 + R) * ww2 - sww;                                                 //  right side
      pxbmod = GPNFD(pixels3+ww4*3,ww3+sww,ss_hh);
      gdk_cairo_set_source_pixbuf(ss_cr,pxbmod,ww4,0);
      cairo_paint(ss_cr);
      g_object_unref(pxbmod);

      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04
      g_object_unref(ss_pxbmix);
   }

   ss_pxbmix = gdk_pixbuf_copy(ss_pxbnew);
   ss_cr = gdk_cairo_create(gdkwin);
   gdk_cairo_set_source_pixbuf(ss_cr,ss_pxbmix,0,0);
   cairo_paint(ss_cr);
   cairo_destroy(ss_cr);
   zmainloop();
   g_object_unref(ss_pxbmix);

   return;
}


void * ss_frenchdoor_thread(void *arg)
{
   using namespace ss_frenchdoor_names;

   int      index = *((int *) arg);
   uint8    *pix1, *pix3;
   int      px1, py1, px3, py3, px3L = -1;
   float    F;

   for (px1 = index; px1 < ww2; px1 += ss_nwt)                                   //  0 >> ww2
   {
      px3 = (1 - R) * px1;                                                       //  0 >> ww2 - X
      if (px3 == px3L) continue;
      px3L = px3;

      for (py1 = 0; py1 < ss_hh; py1++)                                          //  0 >> ss_hh
      {
         F = 0.2 * ss_hh * R * px1 / ww2;                                        //  0 >> max
         py3 = F + (1.0 * py1 / ss_hh) * (ss_hh - 2 * F);                        //  F >> ss_hh - F
         pix1 = pixels1 + py1 * ss_rs + px1 * 3;
         pix3 = pixels3 + py3 * ss_rs + px3 * 3;
         memcpy(pix3,pix1,3);
      }
   }
   
   for (px1 = ww2 + index; px1 < ss_ww; px1 += ss_nwt)                           //  ww2 >> ss_ww
   {
      px3 = R * ss_ww + (1 - R) * px1;                                           //  ww2 + X >> ss_ww
      if (px3 == px3L) continue;
      px3L = px3;

      for (py1 = 0; py1 < ss_hh; py1++)                                          //  0 >> ss_hh
      {
         F = 0.2 * ss_hh * R * (ss_ww - px1) / ww2;                              //  max >> 0
         py3 = F + (1.0 * py1 / ss_hh) * (ss_hh - 2 * F);                        //  F >> ss_hh - F
         pix1 = pixels1 + py1 * ss_rs + px1 * 3;
         pix3 = pixels3 + py3 * ss_rs + px3 * 3;
         memcpy(pix3,pix1,3);
      }
   }

   tbusy[index] = 0;
   pthread_exit(0);
   return 0;
}


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

//  position zoomed image in pixbuf for display in window
//  file: input image file
//  zoom:  image zoom size, e.g. 2.0 = 2x window size (1x = fit window)
//  mode:  1 = initialize for new image: zoom = full zoom factor
//         2 = get zoomed image: 1.0 <= zoom <= full zoom factor
//         3 = release resources
//  fast:  1 = fast zoom, 0 = slow zoom, highest quality

PIXBUF * ss_zoom_posn(char *file, int mode, float zoom, int fast)                //  15.07
{
   static PIXBUF  *pxb1 = 0, *pxb2 = 0, *pxb3 = 0;
   static int     ww1, hh1, ww2, hh2, ww3, hh3;
   static float   cx, cy;
   static float   zoomax;
   static int     ox1, oy1, ox2, oy2, ox3, oy3, ox4, oy4, ox5, oy5;

   GError   *gerror = 0;
   float    Rm, Rw, dR, R;

   if (mode == 1)
   {
      pxb1 = gdk_pixbuf_new_from_file(file,&gerror);                             //  load 1x image file
      if (! pxb1) {
         zmessageACK(Mwin,gerror->message);
         return 0;
      }

      pxb2 = gdk_pixbuf_stripalpha(pxb1);                                        //  stip alpha channel if present      15.08
      if (pxb2) {
         g_object_unref(pxb1);
         pxb1 = pxb2;
      }

      ww1 = gdk_pixbuf_get_width(pxb1);                                          //  image dimensions, 1x
      hh1 = gdk_pixbuf_get_height(pxb1);

      zoomax = zoom;                                                             //  full zoom factor

      ww2 = ss_ww * zoomax;                                                      //  image dimensions for full zoom
      hh2 = ss_hh * zoomax;                                                      //    = zoomax * window size

      Rm = 1.0 * ww1 / hh1;                                                      //  image width/height ratio
      Rw = 1.0 * ww2 / hh2;                                                      //  window width/height ratio
      dR = fabsf(Rm - Rw) / Rw;                                                  //  difference ratio

      if (dR <= 0.01 * ss_cliplimit) {                                           //  difference is within user limit
         if (Rw >= Rm) {
            ww1 = ww2;                                                           //  width fits window
            hh1 = ww1 / Rm;                                                      //  height will be cut
         }
         else {
            hh1 = hh2;                                                           //  height fits window
            ww1 = hh1 * Rm;                                                      //  width will be cut
         }
      }
      else {                                                                     //  difference is too great
         if (Rw >= Rm) {
            hh1 = hh2;                                                           //  height fits window
            ww1 = hh1 * Rm;                                                      //  width will get margins
         }
         else {
            ww1 = ww2;                                                           //  width fits window
            hh1 = ww1 / Rm;                                                      //  height will get margins
         }
      }

      pxb2 = gdk_pixbuf_scale_simple(pxb1,ww1,hh1,BILINEAR);                     //  rescaled image, 1x
      g_object_unref(pxb1);
      pxb1 = pxb2;
      pxb2 = 0;

      cx = 0.01 * ss_zoomlocx;                                                   //  zoom center, 0.5/0.5 = image midpoint
      cy = 0.01 * ss_zoomlocy;

      cx = cx * ww1;                                                             //  zoom center image position
      cy = cy * hh1;

      ox1 = (ww1 - ww2) / 2;                                                     //  origin of image to copy from
      if (ox1 < 0) ox1 = 0;
      oy1 = (hh1 - hh2) / 2;
      if (oy1 < 0) oy1 = 0;

      ox2 = (ww2 - ww1) / 2;                                                     //  origin of image to copy to
      if (ox2 < 0) ox2 = 0;
      oy2 = (hh2 - hh1) / 2;
      if (oy2 < 0) oy2 = 0;

      cx = cx - ox1 + ox2;                                                       //  zoom target center, adjusted
      cy = cy - oy1 + oy2;                                                       //    for clipping and margins
      
      if (ww2 < ww1) ww1 = ww2;                                                  //  copy width
      if (hh2 < hh1) hh1 = hh2;                                                  //  copy height

      pxb2 = gdk_pixbuf_new(GDKRGB,0,8,ww2,hh2);                                 //  pixbuf matching window w/h ratio
      gdk_pixbuf_fill(pxb2,0);                                                   //  for black margins
      gdk_pixbuf_copy_area(pxb1,ox1,oy1,ww1,hh1,pxb2,ox2,oy2);                   //  insert image
      g_object_unref(pxb1);                                                      //  image at zoomax size, clipped
      pxb1 = pxb2;                                                               //    or with added margins, 
      pxb2 = 0;                                                                  //      matching window w/h ratio
      ww1 = ww2;                                                                 //  base input image, zoomax size
      hh1 = hh2;                                                                 //  (pxb1, ww1, hh1)

      ww2 = ww1 / zoomax;                                                        //  final zoomed image size
      hh2 = hh1 / zoomax;                                                        //  (will be scaled to window size)
      
      ox2 = cx - ww2 / 2;                                                        //  upper left corner of
      oy2 = cy - hh2 / 2;                                                        //    final zoomed image
      
      if (ox2 + ww2 > ww1) ox2 = ww1 - ww2;                                      //  keep within image bounds
      if (ox2 < 0) ox2 = 0;
      if (oy2 + hh2 > hh1) oy2 = hh1 - hh2;
      if (oy2 < 0) oy2 = 0;
      
      ox3 = ox2 + ww2;                                                           //  lower right corner of 
      oy3 = oy2 + hh2;                                                           //    final zoomed image

      return 0;
   }

   if (mode == 2)
   {
      R = (zoom - 1) / (zoomax - 1);                                             //  R = 0 ... 1

      ox4 = ox2 * R;                                                             //  upper left corner
      oy4 = oy2 * R;                                                             //    = 0/0 ... ox2/oy2
      
      ox5 = ww1 - (ww1 - ox3) * R;                                               //  lower right corner
      oy5 = hh1 - (hh1 - oy3) * R;                                               //    = ww1/hh1 ... ox3/oy3
      
      ww3 = ox5 - ox4;                                                           //  size = ww1/hh1 ... ww2/hh2
      hh3 = oy5 - oy4;

      pxb2 = gdk_pixbuf_new_subpixbuf(pxb1,ox4,oy4,ww3,hh3);                     //  image segment to display
      if (! pxb2) zappcrash("ss_zoom() %d %d %d %d",ox4,oy4,ww3,hh3);
      
      if (fast)                                                                  //  fast rescale, less quality
         pxb3 = pixbuf_rescale_fast(pxb2,ss_ww,ss_hh);
      else 
         pxb3 = gdk_pixbuf_scale_simple(pxb2,ss_ww,ss_hh,BILINEAR);              //  scale to window size, high quality
      
      g_object_unref(pxb2);
      pxb2 = 0;
      return pxb3;
   }

   if (mode == 3)                                                                //  release resources
   {
      if (pxb1) g_object_unref(pxb1);
      if (pxb2) g_object_unref(pxb2);
      pxb1 = pxb2 = 0;
      return 0;
   }

   return 0;
}


//  slowly zoom-in on the image (Ken Burns effect)

void ss_zoomin()                                                                 //  15.07
{
   PIXBUF   *pixbuf;
   float    zoom;
   float    zoominc = (ss_zoomsize - 1.0) / ss_zoomsteps;                        //  zoom increment
   
   ss_zoom_posn(ss_newfile,1,ss_zoomsize,0);                                     //  initialize for new image

   for (zoom = 1.0; zoom < ss_zoomsize; zoom += zoominc)                         //  loop fit window ... zoom size
   {
      pixbuf = ss_zoom_posn(0,2,zoom,1);                                         //  get zoomed image at zoom size
      ss_cr = gdk_cairo_create(gdkwin);
      gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,0,0);                             //  paint
      cairo_paint(ss_cr);
      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04
      g_object_unref(pixbuf);
      if (ss_escape) break;
   }

   zoom = ss_zoomsize;
   pixbuf = ss_zoom_posn(0,2,zoom,0);                                            //  last image, high quality rescale
   ss_cr = gdk_cairo_create(gdkwin);
   gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,0,0);                                //  paint
   cairo_paint(ss_cr);
   cairo_destroy(ss_cr);
   zmainloop();
   
   ss_zoom_posn(0,3,0,0);                                                        //  free resources
   g_object_unref(ss_pxbnew);                                                    //  retain final zoomed image
   ss_pxbnew = pixbuf;                                                           //    for next transition
   return;
}


//  slowly zoom-out from initial image center point

void ss_zoomout()                                                                //  15.07
{
   PIXBUF   *pixbuf;
   float    zoom;
   float    zoominc = (ss_zoomsize - 1.0) / ss_zoomsteps;                        //  zoom increment
   
   ss_zoom_posn(ss_newfile,1,ss_zoomsize,0);                                     //  initialize for new image

   for (zoom = ss_zoomsize; zoom > 1.0; zoom -= zoominc)                         //  loop zoom size ... fit window
   {
      pixbuf = ss_zoom_posn(0,2,zoom,1);                                         //  get zoomed image at zoom size
      ss_cr = gdk_cairo_create(gdkwin);
      gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,0,0);                             //  paint
      cairo_paint(ss_cr);
      cairo_destroy(ss_cr);
      zmainloop();                                                               //  16.04
      g_object_unref(pixbuf);
      if (ss_escape) break;
   }

   zoom = 1.0;
   pixbuf = ss_zoom_posn(0,2,zoom,0);                                            //  last image, high quality rescale
   ss_cr = gdk_cairo_create(gdkwin);
   gdk_cairo_set_source_pixbuf(ss_cr,pixbuf,0,0);                                //  paint
   cairo_paint(ss_cr);
   cairo_destroy(ss_cr);
   zmainloop();

   ss_zoom_posn(0,3,0,0);                                                        //  free resources
   g_object_unref(ss_pxbnew);                                                    //  retain final zoomed image
   ss_pxbnew = pixbuf;                                                           //    for next transition
   return;
}


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

//  set desktop wallpaper from the current fotoxx image

void m_setdesktop(GtkWidget *, cchar *)                                          //  15.12
{
   cchar    *setbg = "gsettings set org.gnome.desktop.background picture-uri";

   F1_help_topic = "set_desktop";
   
   if (! curr_file) {
      zmessageACK(Mwin,"no current file");
      return;
   }

   snprintf(command,CCC,"%s \'file://%s\'",setbg,curr_file);                     //  gsettings ... 'file:///...filename.jpg'
   shell_quiet(command);
   return;
}


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

//  Cycle desktop wallpaper images from a Fotoxx album.
//  Runs in background without a window or GUI.
//  Command line: $ fotoxx -cycledesktop album seconds &
//  To stop the process: $ killall fotoxx

void m_cycledesktop(GtkWidget*, cchar *menu)                                     //  15.06
{
   F1_help_topic = "cycle_desktop";
   showz_userguide("cycle_desktop");
   return;
}


namespace cycledesktop_names
{
   VOL int  nfiles = 0;                                                          //  file count
   char     **filelist = 0;                                                      //  file list from album
}


//  called from main() at Fotoxx startup 

void run_cycledesktop(cchar *album, cchar *aseconds)                             //  15.06
{
   using namespace cycledesktop_names;
   
   int cycledesktop_timerfunc(void *);

   char     albumfile[300], *file;
   char     buff[XFCC];
   int      ii, seconds;
   FTYPE    ftype;
   FILE     *fid;

   snprintf(albumfile,300,"%s/%s",albums_dirk,album);

   fid = fopen(albumfile,"r");                                                   //  open album file
   if (! fid) {
      printz("bad album name \n");
      exit(1);
   }
   
   seconds = atoi(aseconds);
   if (seconds < 10) {
      printz("seconds must be at least 10 \n");
      exit(1);
   }

   while (true) {                                                                //  read all image file names
      file = fgets_trim(buff,XFCC,fid);
      if (! file) break;
      ftype = image_file_type(file);                                             //  screen out deleted files
      if (ftype != IMAGE) continue;
      nfiles++;                                                                  //  count the files
   }

   fclose(fid);
      
   if (! nfiles) {
      printz("empty album \n");
      exit(1);
   }

   filelist = (char **) zmalloc(nfiles * sizeof(char *));

   fid = fopen(albumfile,"r");                                                   //  open album file again
      
   ii = 0;

   while (ii < nfiles) {                                                         //  read all image file names
      file = fgets_trim(buff,XFCC,fid);
      if (! file) break;
      ftype = image_file_type(file);                                             //  screen out deleted files
      if (ftype != IMAGE) continue;
      filelist[ii] = zstrdup(file);                                              //  add to file list
      ii++;
   }

   fclose(fid);

   if (ii != nfiles) zappcrash("ii: %d  nfiles: %d",ii,nfiles);
   
   if (! cycledesktop_album || ! strmatch(album,cycledesktop_album))             //  new album 
      cycledesktop_index = 0;                                                    //  reset file index
   
   if (cycledesktop_album) zfree(cycledesktop_album);                            //  set album name
   cycledesktop_album = zstrdup(album);

   save_params();                                                                //  save parameters, album name and index

   cycledesktop_timerfunc(0);                                                    //  first interval, immediate
   g_timeout_add_seconds(seconds,cycledesktop_timerfunc,0);                      //  call every time interval

   return;
}


//  timer function, called at intervals of "seconds"

int cycledesktop_timerfunc(void *)
{
   using namespace cycledesktop_names;

   cchar    *setbg = "gsettings set org.gnome.desktop.background picture-uri";
   char     *file;
   int      err;
   
   load_params();                                                                //  load parameters
   if (cycledesktop_index >= nfiles) cycledesktop_index = 0;                     //  last image file, back to first
   file = filelist[cycledesktop_index];                                          //  next image file: /.../filename.jpg
   cycledesktop_index++;                                                         //  update index
   save_params();                                                                //  save parameters
   
   snprintf(command,CCC,"%s \'file://%s\'",setbg,file);                          //  gsettings ... 'file:///.../filename.jpg'
   err = shell_quiet(command);
   if (err) exit(1);
   return 1;
}



