/* Copyright (c) 2017 imv authors 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "imv.h" #include #include #include #include #include #include #include #include #include "commands.h" #include "list.h" #include "loader.h" #include "texture.h" #include "navigator.h" #include "viewport.h" #include "util.h" enum scaling_mode { SCALING_NONE, SCALING_DOWN, SCALING_FULL, SCALING_MODE_COUNT }; static const char *scaling_label[] = { "actual size", "best fit", "perfect fit" }; enum background_type { BACKGROUND_SOLID, BACKGROUND_CHEQUERED, BACKGROUND_TYPE_COUNT }; struct imv { bool quit; bool fullscreen; bool overlay_enabled; bool nearest_neighbour; bool need_redraw; bool need_rescale; bool recursive_load; bool cycle_input; bool list_at_exit; bool paths_from_stdin; enum scaling_mode scaling_mode; enum background_type background_type; struct { unsigned char r, g, b; } background_color; unsigned long slideshow_image_duration; unsigned long slideshow_time_elapsed; char *font_name; struct imv_navigator *navigator; struct imv_loader *loader; struct imv_commands *commands; struct imv_texture *texture; struct imv_viewport *view; char *input_buffer; char *starting_path; struct pollfd stdin_fd; SDL_Window *window; SDL_Renderer *renderer; TTF_Font *font; SDL_Texture *background_texture; bool sdl_init; bool ttf_init; }; void command_quit(struct imv_list *args, void *data); void command_pan(struct imv_list *args, void *data); void command_select_rel(struct imv_list *args, void *data); void command_select_abs(struct imv_list *args, void *data); void command_zoom(struct imv_list *args, void *data); void command_remove(struct imv_list *args, void *data); void command_fullscreen(struct imv_list *args, void *data); void command_overlay(struct imv_list *args, void *data); static bool setup_window(struct imv *imv); static void handle_event(struct imv *imv, SDL_Event *event); static void render_window(struct imv *imv); struct imv *imv_create(void) { struct imv *imv = malloc(sizeof(struct imv)); imv->quit = false; imv->fullscreen = false; imv->overlay_enabled = false; imv->nearest_neighbour = false; imv->need_redraw = true; imv->need_rescale = true; imv->recursive_load = false; imv->scaling_mode = SCALING_NONE; imv->cycle_input = true; imv->list_at_exit = false; imv->paths_from_stdin = false; imv->background_color.r = imv->background_color.g = imv->background_color.b = 0; imv->slideshow_image_duration = 0; imv->slideshow_time_elapsed = 0; imv->font_name = strdup("Monospace:24"); imv->navigator = imv_navigator_create(); imv->loader = imv_loader_create(); imv->commands = imv_commands_create(); imv->input_buffer = NULL; imv->starting_path = NULL; imv->window = NULL; imv->renderer = NULL; imv->font = NULL; imv->background_texture = NULL; imv->sdl_init = false; imv->ttf_init = false; imv_command_register(imv->commands, "quit", &command_quit); imv_command_register(imv->commands, "pan", &command_pan); imv_command_register(imv->commands, "select_rel", &command_select_rel); imv_command_register(imv->commands, "select_abs", &command_select_abs); imv_command_register(imv->commands, "zoom", &command_zoom); imv_command_register(imv->commands, "remove", &command_remove); imv_command_register(imv->commands, "fullscreen", &command_fullscreen); imv_command_register(imv->commands, "overlay", &command_overlay); imv_command_alias(imv->commands, "q", "quit"); imv_command_alias(imv->commands, "next", "select_rel 1"); imv_command_alias(imv->commands, "previous", "select_rel -1"); imv_command_alias(imv->commands, "n", "select_rel 1"); imv_command_alias(imv->commands, "p", "select_rel -1"); return imv; } void imv_free(struct imv *imv) { free(imv->font_name); imv_navigator_free(imv->navigator); imv_loader_free(imv->loader); imv_commands_free(imv->commands); if(imv->input_buffer) { free(imv->input_buffer); } if(imv->renderer) { SDL_DestroyRenderer(imv->renderer); } if(imv->window) { SDL_DestroyWindow(imv->window); } if(imv->background_texture) { SDL_DestroyTexture(imv->background_texture); } if(imv->font) { TTF_CloseFont(imv->font); } if(imv->ttf_init) { TTF_Quit(); } if(imv->sdl_init) { SDL_Quit(); } free(imv); } bool imv_parse_args(struct imv *imv, int argc, char **argv) { /* Do not print getopt errors */ opterr = 0; char *argp, *ep = *argv; int o; while((o = getopt(argc, argv, "frasSudxhln:b:e:t:")) != -1) { switch(o) { case 'f': imv->fullscreen = true; break; case 'r': imv->recursive_load = true; break; case 'a': imv->scaling_mode = SCALING_NONE; break; case 's': imv->scaling_mode = SCALING_DOWN; break; case 'S': imv->scaling_mode = SCALING_FULL; break; case 'u': imv->nearest_neighbour = true; break; case 'd': imv->overlay_enabled = true; break; case 'x': imv->cycle_input = false; break; case 'l': imv->list_at_exit = true; break; case 'n': imv->starting_path = optarg; break; case 'e': imv->font_name = strdup(optarg); break; case 'h': fprintf(stdout, "imv %s\n" "See manual for usage information.\n" "\n" "Legal:\n" "This program is free software; you can redistribute it and/or\n" "modify it under the terms of the GNU General Public License\n" "as published by the Free Software Foundation; either version 2\n" "of the License, or (at your option) any later version.\n" "\n" "This software uses the FreeImage open source image library.\n" "See http://freeimage.sourceforge.net for details.\n" "FreeImage is used under the GNU GPLv2.\n" , IMV_VERSION); imv->quit = true; return true; case 'b': if(strcmp("checks", optarg) == 0) { imv->background_type = BACKGROUND_CHEQUERED; } else { imv->background_type = BACKGROUND_SOLID; argp = (*optarg == '#') ? optarg + 1 : optarg; uint32_t n = strtoul(argp, &ep, 16); if(*ep != '\0' || ep - argp != 6 || n > 0xFFFFFF) { fprintf(stderr, "Invalid hex color: '%s'\n", optarg); return false; } imv->background_color.b = n & 0xFF; imv->background_color.g = (n >> 8) & 0xFF; imv->background_color.r = (n >> 16); } break; case 't': imv->slideshow_image_duration = strtoul(optarg, &argp, 10); imv->slideshow_image_duration *= 1000; if (*argp == '.') { long delay = strtoul(++argp, &ep, 10); for (int i = 3 - (ep - argp); i; i--) { delay *= 10; } if (delay < 1000) { imv->slideshow_image_duration += delay; } else { imv->slideshow_image_duration = ULONG_MAX; } } if (imv->slideshow_image_duration == ULONG_MAX) { fprintf(stderr, "Wrong slideshow delay '%s'. Aborting.\n", optarg); return false; } break; case '?': fprintf(stderr, "Unknown argument '%c'. Aborting.\n", optopt); return false; } } argc -= optind; argv += optind; /* if no paths are given as args, expect them from stdin */ if(argc == 0) { imv->paths_from_stdin = true; } if(imv->paths_from_stdin) { imv->stdin_fd.fd = STDIN_FILENO; imv->stdin_fd.events = POLLIN; fprintf(stderr, "Reading paths from stdin..."); char buf[PATH_MAX]; char *stdin_ok; while((stdin_ok = fgets(buf, sizeof(buf), stdin)) != NULL) { size_t len = strlen(buf); if(buf[len-1] == '\n') { buf[--len] = 0; } if(len > 0) { imv_add_path(imv, buf); break; } } if(!stdin_ok) { fprintf(stderr, " no input!\n"); return false; } fprintf(stderr, "\n"); } return true; } void imv_check_stdin_for_paths(struct imv *imv) { /* check stdin to see if we've been given any new paths to load */ if(poll(&imv->stdin_fd, 1, 10) != 1 || imv->stdin_fd.revents & (POLLERR|POLLNVAL)) { fprintf(stderr, "error polling stdin"); imv->quit = true; return; } if(imv->stdin_fd.revents & (POLLIN|POLLHUP)) { char buf[PATH_MAX]; if(fgets(buf, sizeof(buf), stdin) == NULL && ferror(stdin)) { clearerr(stdin); return; } if(feof(stdin)) { imv->paths_from_stdin = false; fprintf(stderr, "done with stdin\n"); return; } size_t len = strlen(buf); if(buf[len-1] == '\n') { buf[--len] = 0; } if(len > 0) { imv_add_path(imv, buf); imv->need_redraw = true; } } } void imv_add_path(struct imv *imv, const char *path) { imv_navigator_add(imv->navigator, path, imv->recursive_load); } bool imv_run(struct imv *imv) { if(!setup_window(imv)) return false; imv->quit = 0; /* cache current image's dimensions */ int iw = 0; int ih = 0; while(!imv->quit) { SDL_Event e; while(!imv->quit && SDL_PollEvent(&e)) { handle_event(imv, &e); } /* if we're quitting, don't bother drawing any more images */ if(imv->quit) { break; } /* check if an image failed to load, if so, remove it from our image list */ char *err_path = imv_loader_get_error(imv->loader); if(err_path) { imv_navigator_remove(imv->navigator, err_path); /* special case: the image came from stdin */ /* if (strncmp(err_path, "-", 2) == 0) { */ /* free(stdin_buffer); */ /* stdin_buffer_size = 0; */ /* if (stdin_error != 0) { */ /* errno = stdin_error; */ /* perror("Failed to load image from standard input"); */ /* errno = 0; */ /* } */ /* } */ free(err_path); } /* Check if navigator wrapped around paths lists */ if(!imv->cycle_input && imv_navigator_wrapped(imv->navigator)) { break; } /* if the user has changed image, start loading the new one */ if(imv_navigator_poll_changed(imv->navigator)) { const char *current_path = imv_navigator_selection(imv->navigator); if(!current_path) { if(!imv->paths_from_stdin) { fprintf(stderr, "No input files left. Exiting.\n"); imv->quit = true; } continue; } char title[1024]; snprintf(title, sizeof(title), "imv - [%i/%i] [LOADING] %s [%s]", imv->navigator->cur_path + 1, imv->navigator->num_paths, current_path, scaling_label[imv->scaling_mode]); imv_viewport_set_title(imv->view, title); imv_loader_load(imv->loader, current_path, "", 0 /*stdin_buffer, stdin_buffer_size*/); imv->view->playing = 1; } /* check if a new image is available to display */ FIBITMAP *bmp; int is_new_image; if(imv_loader_get_image(imv->loader, &bmp, &is_new_image)) { imv_texture_set_image(imv->texture, bmp); iw = FreeImage_GetWidth(bmp); ih = FreeImage_GetHeight(bmp); FreeImage_Unload(bmp); imv->need_redraw = 1; imv->need_rescale += is_new_image; } if(imv->need_rescale) { int ww, wh; SDL_GetWindowSize(imv->window, &ww, &wh); imv->need_rescale = 0; if(imv->scaling_mode == SCALING_NONE || (imv->scaling_mode == SCALING_DOWN && ww > iw && wh > ih)) { imv_viewport_scale_to_actual(imv->view, imv->texture); } else { imv_viewport_scale_to_window(imv->view, imv->texture); } } if(imv->need_redraw) { render_window(imv); SDL_RenderPresent(imv->renderer); } if(imv->paths_from_stdin) { /* check stdin for any more paths */ imv_check_stdin_for_paths(imv); } else { /* sleep a little bit so we don't waste CPU time */ SDL_Delay(10); } } return 0; } static bool setup_window(struct imv *imv) { if(SDL_Init(SDL_INIT_VIDEO) != 0) { fprintf(stderr, "SDL Failed to Init: %s\n", SDL_GetError()); return false; } imv->sdl_init = true; /* width and height arbitrarily chosen. Perhaps there's a smarter way to * set this */ const int width = 1280; const int height = 720; imv->window = SDL_CreateWindow( "imv", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_RESIZABLE); if(!imv->window) { fprintf(stderr, "SDL Failed to create window: %s\n", SDL_GetError()); return false; } /* we'll use SDL's built-in renderer, hardware accelerated if possible */ imv->renderer = SDL_CreateRenderer(imv->window, -1, 0); if(!imv->renderer) { fprintf(stderr, "SDL Failed to create renderer: %s\n", SDL_GetError()); return false; } /* use the appropriate resampling method */ SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, imv->nearest_neighbour ? "0" : "1"); /* construct a chequered background texture */ if(imv->background_type == BACKGROUND_CHEQUERED) { imv->background_texture = create_chequered(imv->renderer); } /* set up the required fonts and surfaces for displaying the overlay */ TTF_Init(); imv->ttf_init = true; imv->font = load_font(imv->font_name); if(!imv->font) { fprintf(stderr, "Error loading font: %s\n", TTF_GetError()); return false; } imv->texture = imv_texture_create(imv->renderer); imv->view = imv_viewport_create(imv->window); /* put us in fullscren mode to begin with if requested */ if(imv->fullscreen) { imv_viewport_toggle_fullscreen(imv->view); } /* start outside of command mode */ SDL_StopTextInput(); return true; } static void handle_event(struct imv *imv, SDL_Event *event) { const int command_buffer_len = 1024; switch(event->type) { case SDL_QUIT: imv_command_exec(imv->commands, "quit", imv); break; case SDL_TEXTINPUT: strncat(imv->input_buffer, event->text.text, command_buffer_len - 1); imv->need_redraw = true; break; case SDL_KEYDOWN: SDL_ShowCursor(SDL_DISABLE); if(imv->input_buffer) { /* in command mode, update the buffer */ if(event->key.keysym.sym == SDLK_ESCAPE) { SDL_StopTextInput(); free(imv->input_buffer); imv->input_buffer = NULL; imv->need_redraw = true; } else if(event->key.keysym.sym == SDLK_RETURN) { imv_command_exec(imv->commands, imv->input_buffer, imv); SDL_StopTextInput(); free(imv->input_buffer); imv->input_buffer = NULL; imv->need_redraw = true; } else if(event->key.keysym.sym == SDLK_BACKSPACE) { const size_t len = strlen(imv->input_buffer); if(len > 0) { imv->input_buffer[len - 1] = '\0'; imv->need_redraw = true; } } return; } switch (event->key.keysym.sym) { case SDLK_SEMICOLON: if(event->key.keysym.mod & KMOD_SHIFT) { SDL_StartTextInput(); imv->input_buffer = malloc(command_buffer_len); imv->input_buffer[0] = '\0'; imv->need_redraw = true; } break; case SDLK_q: imv->quit = true; imv_command_exec(imv->commands, "quit", imv); break; case SDLK_LEFTBRACKET: case SDLK_LEFT: imv_command_exec(imv->commands, "select_rel -1", imv); break; case SDLK_RIGHTBRACKET: case SDLK_RIGHT: imv_command_exec(imv->commands, "select_rel 1", imv); break; case SDLK_EQUALS: case SDLK_PLUS: case SDLK_i: case SDLK_UP: imv_viewport_zoom(imv->view, imv->texture, IMV_ZOOM_KEYBOARD, 1); break; case SDLK_MINUS: case SDLK_o: case SDLK_DOWN: imv_viewport_zoom(imv->view, imv->texture, IMV_ZOOM_KEYBOARD, -1); break; case SDLK_s: if(!event->key.repeat) { imv->scaling_mode++; imv->scaling_mode %= SCALING_MODE_COUNT; } /* FALLTHROUGH */ case SDLK_r: if(!event->key.repeat) { imv->need_rescale = true; imv->need_redraw = true; } break; case SDLK_a: if(!event->key.repeat) { imv_viewport_scale_to_actual(imv->view, imv->texture); } break; case SDLK_c: if(!event->key.repeat) { imv_viewport_center(imv->view, imv->texture); } break; case SDLK_j: imv_command_exec(imv->commands, "pan 0 -50", imv); break; case SDLK_k: imv_command_exec(imv->commands, "pan 0 50", imv); break; case SDLK_h: imv_command_exec(imv->commands, "pan 50 0", imv); break; case SDLK_l: imv_command_exec(imv->commands, "pan -50 0", imv); break; case SDLK_x: if(!event->key.repeat) { imv_command_exec(imv->commands, "remove", imv); } break; case SDLK_f: if(!event->key.repeat) { imv_command_exec(imv->commands, "fullscreen", imv); } break; case SDLK_PERIOD: imv_loader_load_next_frame(imv->loader); break; case SDLK_SPACE: if(!event->key.repeat) { imv_viewport_toggle_playing(imv->view); } break; case SDLK_p: if(!event->key.repeat) { puts(imv_navigator_selection(imv->navigator)); } break; case SDLK_d: if(!event->key.repeat) { imv_command_exec(imv->commands, "overlay", imv); } break; case SDLK_t: if(event->key.keysym.mod & (KMOD_SHIFT|KMOD_CAPS)) { if(imv->slideshow_image_duration >= 1000) { imv->slideshow_image_duration -= 1000; } } else { imv->slideshow_image_duration += 1000; } imv->need_redraw = true; break; } break; case SDL_MOUSEWHEEL: imv_viewport_zoom(imv->view, imv->texture, IMV_ZOOM_MOUSE, event->wheel.y); SDL_ShowCursor(SDL_ENABLE); break; case SDL_MOUSEMOTION: if(event->motion.state & SDL_BUTTON_LMASK) { imv_viewport_move(imv->view, event->motion.xrel, event->motion.yrel); } SDL_ShowCursor(SDL_ENABLE); break; case SDL_WINDOWEVENT: imv_viewport_update(imv->view, imv->texture); break; } } static void render_window(struct imv *imv) { char title[1024]; int ww, wh; SDL_GetWindowSize(imv->window, &ww, &wh); /* update window title */ const char *current_path = imv_navigator_selection(imv->navigator); int len = snprintf(title, sizeof(title), "imv - [%i/%i] [%ix%i] [%.2f%%] %s [%s]", imv->navigator->cur_path + 1, imv->navigator->num_paths, imv->texture->width, imv->texture->height, 100.0 * imv->view->scale, current_path, scaling_label[imv->scaling_mode]); if(imv->slideshow_image_duration >= 1000) { len += snprintf(title + len, sizeof(title) - len, "[%lu/%lus]", imv->slideshow_time_elapsed / 1000 + 1, imv->slideshow_image_duration / 1000); } imv_viewport_set_title(imv->view, title); /* first we draw the background */ if(imv->background_type == BACKGROUND_SOLID) { /* solid background */ SDL_SetRenderDrawColor(imv->renderer, imv->background_color.r, imv->background_color.g, imv->background_color.b, 255); SDL_RenderClear(imv->renderer); } else { /* chequered background */ int img_w, img_h; SDL_QueryTexture(imv->background_texture, NULL, NULL, &img_w, &img_h); /* tile the texture so it fills the window */ for(int y = 0; y < wh; y += img_h) { for(int x = 0; x < ww; x += img_w) { SDL_Rect dst_rect = {x,y,img_w,img_h}; SDL_RenderCopy(imv->renderer, imv->background_texture, NULL, &dst_rect); } } } /* draw our actual texture */ imv_texture_draw(imv->texture, imv->view->x, imv->view->y, imv->view->scale); /* if the overlay needs to be drawn, draw that too */ if(imv->overlay_enabled && imv->font) { SDL_Color fg = {255,255,255,255}; SDL_Color bg = {0,0,0,160}; imv_printf(imv->renderer, imv->font, 0, 0, &fg, &bg, "%s", title + strlen("imv - ")); } /* draw command entry bar if needed */ if(imv->input_buffer && imv->font) { SDL_Color fg = {255,255,255,255}; SDL_Color bg = {0,0,0,160}; imv_printf(imv->renderer, imv->font, 0, wh - TTF_FontHeight(imv->font), &fg, &bg, ":%s", imv->input_buffer); } /* redraw complete, unset the flag */ imv->need_redraw = false; } void command_quit(struct imv_list *args, void *data) { (void)args; struct imv *imv = data; imv->quit = 1; } void command_pan(struct imv_list *args, void *data) { struct imv *imv = data; if(args->len != 3) { return; } long int x = strtol(args->items[1], NULL, 10); long int y = strtol(args->items[2], NULL, 10); imv_viewport_move(imv->view, x, y); } void command_select_rel(struct imv_list *args, void *data) { struct imv *imv = data; if(args->len != 2) { return; } long int index = strtol(args->items[1], NULL, 10); imv_navigator_select_rel(imv->navigator, index); imv->slideshow_time_elapsed = 0; } void command_select_abs(struct imv_list *args, void *data) { (void)args; (void)data; } void command_zoom(struct imv_list *args, void *data) { (void)args; (void)data; } void command_remove(struct imv_list *args, void *data) { (void)args; struct imv *imv = data; char* path = strdup(imv_navigator_selection(imv->navigator)); imv_navigator_remove(imv->navigator, path); free(path); imv->slideshow_time_elapsed = 0; } void command_fullscreen(struct imv_list *args, void *data) { (void)args; struct imv *imv = data; imv_viewport_toggle_fullscreen(imv->view); } void command_overlay(struct imv_list *args, void *data) { (void)args; struct imv *imv = data; imv->overlay_enabled = !imv->overlay_enabled; imv->need_redraw = true; } /* vim:set ts=2 sts=2 sw=2 et: */