/*
* image viewer, for framebuffer devices
*
*   (c) 1998-2002 Gerd Knorr <[email protected]>
*
*/

#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <getopt.h>
#include <math.h>
#include <setjmp.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>

#include "loader.h"
#include "dither.h"
#include "fbtools.h"
#include "fs.h"

#include "jpeglib.h"

#define TRUE            1
#define FALSE           0
#define MAX(x,y)        ((x)>(y)?(x):(y))
#define MIN(x,y)        ((x)<(y)?(x):(y))

#define KEY_EOF       -1        /* ^D */
#define KEY_ESC       -2
#define KEY_SPACE     -3
#define KEY_Q         -4
#define KEY_PGUP      -5
#define KEY_PGDN      -6
#define KEY_TIMEOUT   -7
#define KEY_TAGFILE   -8
#define KEY_PLUS      -9
#define KEY_MINUS    -10

#define DEFAULT_DEVICE  "/dev/fb0"

/* ---------------------------------------------------------------------- */

/* variables for read_image */
unsigned long   lut_red[256], lut_green[256], lut_blue[256];
int             dither = FALSE, pcd_res = 3, steps = 50;
int             textreading = 0, visible = 1, redraw = 0;
int             new_image, left, top;

/* file list */
char            **files;
int             fileanz, filenr;

char                       *fbdev = NULL;
char                       *mode  = NULL;
int                        fd, switch_last, debug;

unsigned short red[256],  green[256],  blue[256];
struct fb_cmap cmap  = { 0, 256, red,  green,  blue };

static float fbgamma = 1;

/* Command line options. */
struct option fbi_options[] = {
   {"version",    no_argument,       NULL, 'v'},  /* version */
   {"help",       no_argument,       NULL, 'h'},  /* help */
   {"device",     required_argument, NULL, 'd'},  /* device */
   {"mode",       required_argument, NULL, 'm'},  /* video mode */
   {"gamma",      required_argument, NULL, 'g'},  /* set gamma */
   {"quiet",      no_argument,       NULL, 'q'},  /* quiet */
   {"scroll",     required_argument, NULL, 's'},  /* set scrool */
   {"timeout",    required_argument, NULL, 't'},  /* timeout value */
   {"resolution", required_argument, NULL, 'r'},  /* select resolution */
   {"random",     no_argument,       NULL, 'u'},  /* randomize images */
   {"font",       required_argument, NULL, 'f'},  /* font */
   {"autozoom",   no_argument,       NULL, 'a'},
   {0,0,0,0}
};

/* font handling */
struct fs_font *f;
char *x11_font = "10x20";
char *fontname = NULL;

/* ---------------------------------------------------------------------- */

static void
version(void)
{
   fprintf(stderr, "fbi version " VERSION
           " (c) 1999-2001 Gerd Knorr; compiled on %s.\n", __DATE__ );
}

static void
usage(char *name)
{
   char           *h;

   if (NULL != (h = strrchr(name, '/')))
       name = h+1;
   fprintf(stderr,
           "\n"
           "This program displays images using the Linux framebuffer device.\n"
           "Supported formats: PhotoCD, jpeg, ppm, gif, tiff, xwd, bmp, png.\n"
           "It tries to use ImageMagick's convert for unknown file formats.\n"
           "\n"
           "  Usage: %s [ options ] file1 file2 ... fileN\n"
           "\n"
           "    --help       [-h]      Print this text\n"
           "    --version    [-v]      Show the fbi version number\n"
           "    --device     [-d] dev  Framebuffer device [%s]\n"
           "    --mode       [-m] mode Video mode (must be listed in /etc/fb.modes)\n"
           "                           - Default is current mode.\n"
           "    --gamma      [-g] f    Set gamma\n"
           "    --scroll     [-s] n    Set scroll steps in pixels (default: 50)\n"
           "    --quiet      [-q]      Be quiet: don't print anything at all\n"
           "    --timeout    [-t] n    Load next image after N sec without any keypress\n"
           "    --resolution [-r] n    Select resolution [1..5] (PhotoCD)\n"
           "    --random     [-u]      Show file1 .. fileN in a random order\n"
           "    --font       [-f] fn   Use font fn (either console psf file or\n"
           "                           X11 font spec if a font server is available\n"
           "    --autozoom   [-a]      Automagically pick useful zoom factor.\n"
           "\n"
           "Large images can be scrolled using the cursor keys.  Zoom in/out\n"
           "works with '+' and '-'.  Use ESC or 'q' to quit.  Space and PgDn\n"
           "show the next, PgUp shows the previous image. Jumping to a image\n"
           "works with <number>g.  Return acts like Space but additionally\n"
           "prints the filename of the currently displayed image to stdout.\n"
           "when using the slideshow mode, '--timeout' [-t], pressing 'p' will\n"
           "pause on the current image, and '--random' [-u] will randomize the\n"
           "order of the images.\n"
           "\n",
           name, fbdev ? fbdev : "/dev/fb0");
}

/* ---------------------------------------------------------------------- */

static void
text_init(char *font)
{
   char   *fonts[2] = { font, NULL };

   if (NULL == f)
       f = fs_consolefont(font ? fonts : NULL);
#ifndef X_DISPLAY_MISSING
   if (NULL == f && 0 == fs_connect(NULL))
       f = fs_open(font ? font : x11_font);
#endif
   if (NULL == f) {
       fprintf(stderr,"no font available\n");
       exit(1);
   }
}

static void
text_out(int x, int y, char *str)
{
   y *= f->height;
   y -= f->fontHeader.max_bounds.descent;
   x *= f->width;
   fs_puts(f,x,y,str);
}

/* ---------------------------------------------------------------------- */

struct termios  saved_attributes;
int             saved_fl;

static void
tty_raw(void)
{
   struct termios tattr;

   fcntl(0,F_GETFL,&saved_fl);
   tcgetattr (0, &saved_attributes);

   fcntl(0,F_SETFL,O_NONBLOCK);
   memcpy(&tattr,&saved_attributes,sizeof(struct termios));
   tattr.c_lflag &= ~(ICANON|ECHO);
   tattr.c_cc[VMIN] = 1;
   tattr.c_cc[VTIME] = 0;
   tcsetattr (0, TCSAFLUSH, &tattr);
}

static void
tty_restore(void)
{
   fcntl(0,F_SETFL,saved_fl);
   tcsetattr (0, TCSANOW, &saved_attributes);
}

static void
console_switch(int is_busy)
{
   switch (fb_switch_state) {
   case FB_REL_REQ:
       fb_switch_release();
   case FB_INACTIVE:
       visible = 0;
       break;
   case FB_ACQ_REQ:
       fb_switch_acquire();
   case FB_ACTIVE:
       fb_memset(fb_mem,0,fb_fix.smem_len);
       ioctl(fd,FBIOPAN_DISPLAY,&fb_var);
       if (is_busy)
           text_out(0,0,"still busy, please wait ...");
       visible = 1;
       redraw = 1;
       break;
   default:
       break;
   }
   switch_last = fb_switch_state;
   return;
}

/* ---------------------------------------------------------------------- */

static char *
read_image(char *filename, int *gray, int *width, int *height)
{
   char command[1024];
   static char *image = NULL;
   struct ida_loader *loader;
   struct ida_image_info info;
   char blk[512];
   FILE *fp;
   void *data;
   int l,y;

   if (image) {
       free(image);
       image = NULL;
   }
   new_image = 1;

   /* open file */
   if (NULL == (fp = fopen(filename, "r"))) {
       fprintf(stderr,"open %s: %s\n",filename,strerror(errno));
       return NULL;
   }
   memset(blk,0,sizeof(blk));
   fread(blk,1,sizeof(blk),fp);
   rewind(fp);

   /* pick loader */
   for (l = 0;; l++) {
       loader = loaders[l];
       if (NULL == loader) {
           /* no loader found, try to use ImageMagick's convert */
           sprintf(command,"convert \"%s\" ppm:-",filename);
           if (NULL == (fp = popen(command,"r")))
               return NULL;
           loader = &ppm_loader;
           break;
       }
       if (NULL == loader->magic)
           break;
       if (0 == memcmp(blk+loader->moff,loader->magic,loader->mlen))
           break;
   }

   /* load image */
   data = loader->init(fp,filename,&info);
   if (NULL == data) {
       fprintf(stderr,"loading %s [%s] FAILED\n",filename,loader->name);
       return NULL;
   }
   *width  = info.width;
   *height = info.height;
   *gray   = 0;
   image = malloc((*width) * (*height) * 3);
   for (y = 0; y < (*height); y++) {
       if (switch_last != fb_switch_state)
           console_switch(1);
       loader->read(image + (*width)*3*y, y, data);
   }
   loader->done(data);

   return image;
}

/* ---------------------------------------------------------------------- */

static unsigned char *
convert_line(int gray, int bpp, int line, int zoom, int owidth,
            char unsigned *dest, char unsigned *buffer)
{
   unsigned char  *ptr  = (void*)dest;
   unsigned short *ptr2 = (void*)dest;
   unsigned long  *ptr4 = (void*)dest;
   unsigned char  *b;
   int in,out;
   int shift = zoom + 16;

   if (gray) {
       /* grayscale */
       switch (bpp) {
       case 8:
           b = malloc(3*owidth);
           for (out = 0; out < owidth; out++) {
               in = (out << 16) >> shift;
               b[out*3]   = buffer[in];
               b[out*3+1] = buffer[in];
               b[out*3+2] = buffer[in];
           }
           dither_line(b, ptr, line, owidth);
           free(b);
           ptr += owidth;
           return ptr;
       case 15:
       case 16:
           for (out = 0; out < owidth; out++) {
               in = (out << 16) >> shift;
               ptr2[out] = lut_red[buffer[in]] |
                   lut_green[buffer[in]] |
                   lut_blue[buffer[in]];
           }
           ptr2 += owidth;
           return (char*)ptr2;
           break;
       case 24:
           for (out = 0; out < owidth; out++) {
               in = (out << 16) >> shift;
               ptr[3*out+2] = buffer[in];
               ptr[3*out+1] = buffer[in];
               ptr[3*out+0] = buffer[in];
           }
           ptr += owidth * 3;
           return ptr;
       case 32:
           for (out = 0; out < owidth; out++) {
               in = (out << 16) >> shift;
               ptr4[out] = lut_red[buffer[in]] |
                   lut_green[buffer[in]] |
                   lut_blue[buffer[in]];
           }
           ptr4 += owidth;
           return (char*)ptr4;
       }
   } else {
       /* colors */
       switch (fb_var.bits_per_pixel) {
       case 8:
           b = malloc(3*owidth);
           for (out = 0; out < owidth; out++) {
               in = (out << 16) >> shift;
               b[out*3]   = buffer[in*3];
               b[out*3+1] = buffer[in*3+1];
               b[out*3+2] = buffer[in*3+2];
           }
           dither_line(b, ptr, line, owidth);
           free(b);
           ptr += owidth;
           return ptr;
       case 15:
       case 16:
           for (out = 0; out < owidth; out++) {
               in = (out << 16) >> shift;
               ptr2[out] = lut_red[buffer[in*3]] |
                   lut_green[buffer[in*3+1]] |
                   lut_blue[buffer[in*3+2]];
           }
           ptr2 += owidth;
           return (char*)ptr2;
       case 24:
           for (out = 0; out < owidth; out++) {
               in = (out << 16) >> shift;
               ptr[3*out+2] = buffer[3*in+0];
               ptr[3*out+1] = buffer[3*in+1];
               ptr[3*out+0] = buffer[3*in+2];
           }
           ptr += owidth * 3;
           return ptr;
       case 32:
           for (out = 0; out < owidth; out++) {
               in = (out << 16) >> shift;
               ptr4[out] = lut_red[buffer[in*3]] |
                   lut_green[buffer[in*3+1]] |
                   lut_blue[buffer[in*3+2]];
           }
           ptr4 += owidth;
           return (char*)ptr4;
       }
   }
   /* keep compiler happy */
   return NULL;
}

static unsigned char *
transform_image(unsigned char *iimage, int iwidth, int iheight, int gray,
               int zoom, int *owidth, int *oheight)
{
   int in,out,inlength,outlength;
   unsigned char *ptr;
   static char *image = NULL;
   int shift = zoom + 16;

   if (image) {
       free(image);
       image = NULL;
   }

   *owidth  = iwidth;
   *oheight = iheight;
   if (zoom < 0) {
       *owidth  >>= -zoom;
       *oheight >>= -zoom;
   } else {
       *owidth  <<= zoom;
       *oheight <<= zoom;
   }

   inlength  = iwidth * (gray ? 1 : 3);
   outlength = (*owidth) * ((fb_var.bits_per_pixel+7)/8);
   image = ptr = malloc(outlength * (*oheight));
   if (NULL != image) {
       for (out = 0; out < *oheight; out++) {
           if (switch_last != fb_switch_state)
               console_switch(1);
           in = (out << 16) >> shift;
           ptr = convert_line(gray, fb_var.bits_per_pixel, out, zoom,
                              *owidth, ptr, iimage + (in*inlength));
       }
   }
   return image;
}

/* ---------------------------------------------------------------------- */

static void
lut_init(int depth)
{
   register int    i;

   /* init Lookup tables */
   switch (depth) {
   case 15:
       for (i = 0; i < 256; i++) {
          lut_red[i]   = (i & 0xf8) << 7;      /* bits -rrrrr-- -------- */
          lut_green[i] = (i & 0xf8) << 2;      /* bits------gg ggg----- */
          lut_blue[i]  = (i & 0xf8) >> 3;      /* bits-------- ---bbbbb */
       }
       break;
   case 16:
       for (i = 0; i < 256; i++) {
          lut_red[i]   = (i & 0xf8) << 8;      /* bits rrrrr--- -------- */
          lut_green[i] = (i & 0xfc) << 3;      /* bits -----ggg ggg----- */
          lut_blue[i]  = (i & 0xf8) >> 3;      /* bits -------- ---bbbbb */
       }
       break;
   case 24:
       for (i = 0; i < 256; i++) {
          lut_red[i]   = i << 16;      /* byte -r-- */
          lut_green[i] = i << 8;       /* byte --g- */
          lut_blue[i]  = i;            /* byte ---b */
       }
       break;
   }
}

static unsigned short calc_gamma(int n, int max)
{
   int ret =65535.0 * pow((float)n/(max), 1 / fbgamma);
   if (ret > 65535) ret = 65535;
   if (ret <     0) ret =     0;
   return ret;
}

static void
linear_palette(int bit)
{
   int i, size = 256 >> (8 - bit);

   for (i = 0; i < size; i++)
       red[i] = green[i] = blue[i] = calc_gamma(i,size);
}

static void
svga_dither_palette(int r, int g, int b)
{
   int             rs, gs, bs, i;

   rs = 256 / (r - 1);
   gs = 256 / (g - 1);
   bs = 256 / (b - 1);
   for (i = 0; i < 256; i++) {
       red[i]   = calc_gamma(rs * ((i / (g * b)) % r), 255);
       green[i] = calc_gamma(gs * ((i / b) % g),       255);
       blue[i]  = calc_gamma(bs * ((i) % b),           255);
   }
}

static void
svga_display_image(char *image, int width, int height, int xoff, int yoff)
{
   int             dwidth = MIN(width, fb_var.xres);
   int             dheight = MIN(height, fb_var.yres);
   int             data, video, bank, offset, bytes;

   if (!visible)
       return;
   bytes = (fb_var.bits_per_pixel+7)/8;

   /* offset for image data (image > screen, select visible area) */
   offset = (yoff * width + xoff) * bytes;

   /* offset for video memory (image < screen, center image) */
   video = 0, bank = 0;
   if (width < fb_var.xres)
       video += bytes * ((fb_var.xres - width) / 2);
   if (height < fb_var.yres)
       video += fb_fix.line_length * ((fb_var.yres - height) / 2);

   /* go ! */
   for (data = 0;
        data < width * height * bytes && data / width / bytes < dheight;
        data += width * bytes, video += fb_fix.line_length) {
       memcpy(fb_mem+video, &image[data + offset], dwidth * bytes);
   }
}

static int
svga_show(char *image, int width, int height, int timeout)
{
   static int      paused = 0;
   int             rc;
   char            key[11];
   int             nr = 0;
   fd_set          set;
   struct timeval  limit;

   if (NULL == image)
       return KEY_SPACE; /* skip */

   if (new_image) {
       /* start with centered image, if larger than screen */
       if (width > fb_var.xres)
           left = (width - fb_var.xres) / 2;
       if (height > fb_var.yres && !textreading)
           top = (height - fb_var.yres) / 2;
       new_image = 0;
   }

   redraw = 1;
   for (;;) {
       if (redraw) {
           redraw = 0;
           if (height <= fb_var.yres) {
               top = 0;
           } else {
               if (top < 0)
                   top = 0;
               if (top + fb_var.yres > height)
                   top = height - fb_var.yres;
           }
           if (width <= fb_var.xres) {
               left = 0;
           } else {
               if (left < 0)
                   left = 0;
               if (left + fb_var.xres > width)
                   left = width - fb_var.xres;
           }
           svga_display_image(image, width, height, left, top);
       }
       if (switch_last != fb_switch_state) {
           console_switch(0);
           continue;
       }
       FD_SET(0, &set);
       limit.tv_sec = timeout;
       limit.tv_usec = 0;
       rc = select(1, &set, NULL, NULL,
                   (-1 != timeout && !paused) ? &limit : NULL);
       if (switch_last != fb_switch_state) {
           console_switch(0);
           continue;
       }
       if (0 == rc)
           return KEY_TIMEOUT;
       rc = read(0, key, 10);
       if (rc < 1) {
           /* EOF */
           return KEY_EOF;
       }
       key[rc] = 0;

       if (rc == 1 && (*key == 'g' || *key == 'G')) {
           return nr;
       }
       if (rc == 1 && *key >= '0' && *key <= '9') {
           nr = nr * 10 + (*key - '0');
       } else {
           nr = 0;
       }

       if (rc == 1 && (*key == 'q'    || *key == 'Q' ||
                       *key == 'e'    || *key == 'E' ||
                       *key == '\x1b' || *key == '\n')) {
           if (*key == '\n')
               return KEY_TAGFILE;
           if (*key == '\x1b')
               return KEY_ESC;
           return KEY_Q;
       } else if (0 == strcmp(key, " ")) {
           if (textreading && top < height - fb_var.yres) {
               redraw = 1;
               top += (fb_var.yres-100);
           } else {
               return KEY_SPACE;
           }
       } else if (0 == strcmp(key, "\x1b[A") && height > fb_var.yres) {
           redraw = 1;
           top -= steps;
       } else if (0 == strcmp(key, "\x1b[B") && height > fb_var.yres) {
           redraw = 1;
           top += steps;
       } else if (0 == strcmp(key, "\x1b[1~") && height > fb_var.yres) {
           redraw = 1;
           top = 0;
       } else if (0 == strcmp(key, "\x1b[4~")) {
           redraw = 1;
           top = height - fb_var.yres;
       } else if (0 == strcmp(key, "\x1b[D") && width > fb_var.xres) {
           redraw = 1;
           left -= steps;
       } else if (0 == strcmp(key, "\x1b[C") && width > fb_var.xres) {
           redraw = 1;
           left += steps;
       } else if (0 == strcmp(key, "\x1b[5~")) {
           return KEY_PGUP;
       } else if (0 == strcmp(key, "\x1b[6~")) {
           return KEY_PGDN;
       } else if (0 == strcmp(key, "+")) {
           return KEY_PLUS;
       } else if (0 == strcmp(key, "-")) {
           return KEY_MINUS;
       } else if (0 == strcmp(key, "p") ||
                  0 == strcmp(key, "P")) {
           if (-1 != timeout) {
               paused = !paused;
               text_out(0,0, paused ? "pause on " : "pause off");
           }
#if 0
       } else {
           /* testing: find key codes */
           int             i,len;

           char linebuffer[80];
           len = sprintf(linebuffer,"debug: key: ");
           for (i = 0; i < rc; i++)
               len += sprintf(linebuffer+len, "%s%c",
                              key[i] < 0x20 ? "^" : "",
                              key[i] < 0x20 ? key[i] + 0x40 : key[i]);
           text_out(0,0,linebuffer);
#endif
       }
   }
}

/* ---------------------------------------------------------------------- */

int
main(int argc, char *argv[])
{
   int             timeout = -1, verbose = 1;
   int             randomize = -1;
   int             opt_index = 0;
   int             igray,iwidth,iheight;
   int             width, height;
   unsigned char  *iimage = NULL,*image = NULL;
   int             c, rc, zoom = 0, autozoom = 0;
   int             need_read, need_refresh;
   int             rand_one, rand_two;
   char            *rand_temp;
   int             i;

   char            *basename, *line;
   char            linebuffer[128];

   if (NULL != (line = getenv("FRAMEBUFFER")))
       fbdev = line;
   if (NULL != (line = getenv("FBGAMMA")))
       fbgamma = atof(line);
   if (NULL != (line = getenv("FBFONT")))
       fontname = line;

   for (;;) {
       c = getopt_long(argc, argv, "uvahpqr:t:m:d:g:s:f:", fbi_options, &opt_index);
       if (c == -1)
           break;
       switch (c) {
       case 'a':
           autozoom = 1;
           break;
       case 'q':
           verbose = 0;
           break;
       case 'p':
           textreading = 1;
           break;
       case 'g':
           fbgamma = atof(optarg);
           break;
       case 'r':
           pcd_res = atoi(optarg);
           break;
       case 's':
           steps = atoi(optarg);
           break;
       case 't':
           timeout = atoi(optarg);
           break;
       case 'u':
           randomize = 1;
           break;
       case 'd':
           fbdev = optarg;
           break;
       case 'm':
           mode = optarg;
           break;
       case 'f':
           fontname = optarg;
           break;
       case 'v':
           version();
           exit(1);
           break;
       default:
       case 'h':
           usage(argv[0]);
           exit(1);
       }
   }

   if (optind == argc) {
       usage(argv[0]);
       exit(1);
   }
   files = argv + optind;
   fileanz = argc - optind;
   filenr = 0;

   /* Seed random number gen.  (sic) */
   srand((unsigned)time(NULL ) );

   /* Randomize the order of the images */
   if (randomize != -1) {
       /* Naive implementation - could be improved. */
       for( i = 0; i < fileanz; i++ ) {
           rand_one = rand() % fileanz;
           rand_two = rand() % fileanz;
           rand_temp = files[ rand_one ];
           files[ rand_one ] = files[ rand_two ];
           files[ rand_two ] = rand_temp;
       }
   }

   need_read = 1;
   need_refresh = 1;

   text_init(fontname);
   fd = fb_init(fbdev, mode, 0);
   fb_catch_exit_signals();
   fb_switch_init();
   signal(SIGTSTP,SIG_IGN);
   fs_init_fb(255);

   switch (fb_var.bits_per_pixel) {
   case 8:
       svga_dither_palette(8, 8, 4);
       dither = TRUE;
       init_dither(8, 8, 4, 2);
       break;
   case 15:
   case 16:
       if (fb_fix.visual == FB_VISUAL_DIRECTCOLOR)
           linear_palette(5);
       if (fb_var.green.length == 5) {
           lut_init(15);
       } else {
           lut_init(16);
       }
       break;
   case 24:
       if (fb_fix.visual == FB_VISUAL_DIRECTCOLOR)
           linear_palette(8);
       break;
   case 32:
       if (fb_fix.visual == FB_VISUAL_DIRECTCOLOR)
           linear_palette(8);
       lut_init(24);
       break;
   default:
       fprintf(stderr, "Oops: %i bit/pixel ???\n",
               fb_var.bits_per_pixel);
       exit(1);
   }
   if (fb_fix.visual == FB_VISUAL_DIRECTCOLOR ||
       fb_var.bits_per_pixel == 8) {
       if (-1 == ioctl(fd,FBIOPUTCMAP,&cmap)) {
           perror("ioctl FBIOPUTCMAP");
           exit(1);
       }
   }

   if (verbose) {
       sprintf(linebuffer,"video mode:   %ix%i", fb_var.xres, fb_var.yres);
       text_out(0,2,linebuffer);
       text_out(0,3,"ESC,Q:        quit");
       text_out(0,4,"PgDn,space:   next image");
       text_out(0,4,"Return:       print current file to stdout, display next");
       text_out(0,5,"PgUp:         previous image");
       text_out(0,6,"<number>G:    jump to image <number>");
       text_out(0,7,"cursor keys:  scroll large images");
       text_out(0,8,"P:            pause slideshow (if started with -t)");
   }

   /* svga main loop */
   tty_raw();
   for (;;) {
       if (need_read) {
           need_read = 0;
           need_refresh = 1;
           if (verbose && visible) {
               basename = strrchr(files[filenr], '/');
               basename = basename ? basename + 1 : files[filenr];
               sprintf(linebuffer,"loading %s... ", basename);
               text_out(0,0,linebuffer);
           }
           iimage = read_image(files[filenr], &igray, &iwidth, &iheight);
           if (autozoom) {
               width  = iwidth;
               height = iheight;
               zoom   = 0;
               while (width*2 < fb_var.xres  &&  height*2 < fb_var.yres)
                   width <<= 1, height <<= 1, zoom++;
               while (width > fb_var.xres  ||  height > fb_var.yres)
                   width >>= 1, height >>= 1, zoom--;
           }
           if (iimage) {
               if (verbose && visible) {
                   sprintf(linebuffer+strlen(linebuffer),
                           "scaling (%d)... ",zoom);
                   text_out(0,0,linebuffer);
               }
               image = transform_image(iimage,iwidth,iheight,igray,zoom,
                                       &width,&height);
           } else {
               image = NULL;
           }
           if (NULL == image && verbose && visible) {
               strcat(linebuffer,"FAILED ");
               text_out(0,0,linebuffer);
               sleep(1);
           }
       }
       if (need_refresh) {
           need_refresh = 0;
           if (width < fb_var.xres || height < fb_var.yres)
               if (visible)
                   fb_memset(fb_mem,0,fb_fix.smem_len);
       }
       switch (rc = svga_show(image, width, height, timeout)) {
       case KEY_TAGFILE:
           printf("%s\n",files[filenr]);
           /* fall throuth */
       case KEY_SPACE:
           if (filenr != fileanz - 1) {
               filenr++, need_read = 1;
               break;
           }
           /* else fall */
       case KEY_ESC:
       case KEY_Q:
       case KEY_EOF:
           if (visible)
               fb_memset(fb_mem,0,fb_fix.smem_len);
           tty_restore();
           fb_cleanup();
           exit(0);
           break;
       case KEY_PGDN:
           if (filenr < fileanz - 1)
               filenr++, need_read = 1;
           break;
       case KEY_PGUP:
           if (filenr > 0)
               filenr--, need_read = 1;
           break;
       case KEY_TIMEOUT:
           need_read = 1;
           filenr++;
           if (filenr == fileanz)
               filenr = 0;
           break;
       case KEY_PLUS:
       case KEY_MINUS:
           if (rc == KEY_PLUS && zoom < 8) {
               zoom++;
               top  = top * 2 + fb_var.yres/2;
               left = left * 2 + fb_var.xres/2;
               if (height < fb_var.yres)
                   top -= fb_var.yres-height;
               if (width < fb_var.xres)
                   left -= fb_var.xres-width;
           }
           if (rc == KEY_MINUS && zoom > -8) {
               zoom--;
               top  = top  / 2 - fb_var.yres/4;
               left = left / 2 - fb_var.xres/4;
           }
           if (verbose && visible) {
               sprintf(linebuffer,"scaling (%d)... ",zoom);
               text_out(0,0,linebuffer);
           }
           image = transform_image(iimage,iwidth,iheight,igray,zoom,
                                   &width,&height);
           need_refresh = 1;
           break;
       default:
           if (rc > 0 && rc <= fileanz)
               filenr = rc - 1, need_read = 1;
           break;
       }
   }
}