├── .gitignore ├── contrib └── imv-dir ├── files ├── imv ├── imv.desktop ├── imv-dir.desktop └── imv_config ├── src ├── ipc_common.c ├── bitmap.h ├── imv.h ├── bitmap.c ├── log.h ├── image.h ├── ipc.h ├── imv_msg.c ├── commands.h ├── source_private.h ├── binds.h ├── keyboard.h ├── main.c ├── backend.h ├── log.c ├── source.h ├── image.c ├── console.h ├── list.h ├── dummy_window.c ├── navigator.h ├── canvas.h ├── ipc.c ├── backend_librsvg.c ├── window.h ├── commands.c ├── list.c ├── source.c ├── backend_libheif.c ├── backend_libjpeg.c ├── viewport.h ├── backend_libpng.c ├── keyboard.c ├── backend_libtiff.c ├── backend_libnsgif.c ├── xdg-shell-protocol.c ├── console.c ├── binds.c ├── navigator.c ├── backend_freeimage.c ├── viewport.c ├── canvas.c └── x11_window.c ├── README.md ├── subprojects └── inih.wrap ├── AUTHORS ├── CONTRIBUTING ├── .builds ├── freebsd.yml ├── archlinux.yml ├── ubuntu.yml ├── fedora.yml └── debian.yml ├── doc ├── imv-msg.1.txt ├── imv-dir.1.txt ├── imv.5.txt └── imv.1.txt ├── LICENSE ├── test ├── list.c └── navigator.c ├── PACKAGERS.md ├── meson_options.txt ├── meson.build └── CHANGELOG /.gitignore: -------------------------------------------------------------------------------- 1 | imv 2 | *.o 3 | test_* 4 | build/ 5 | -------------------------------------------------------------------------------- /contrib/imv-dir: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec imv -n "$1" "$(dirname "$1")" 3 | -------------------------------------------------------------------------------- /files/imv: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -n "${WAYLAND_DISPLAY}" ]; then 3 | exec imv-wayland "$@" 4 | else 5 | exec imv-x11 "$@" 6 | fi 7 | -------------------------------------------------------------------------------- /src/ipc_common.c: -------------------------------------------------------------------------------- 1 | #include "ipc.h" 2 | 3 | #include 4 | #include 5 | 6 | void imv_ipc_path(char *buf, size_t len, int pid) 7 | { 8 | const char *base = getenv("XDG_RUNTIME_DIR"); 9 | if (!base) { 10 | base = "/tmp"; 11 | } 12 | snprintf(buf, len, "%s/imv-%d.sock", base, pid); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## imv is now hosted on sr.ht 2 | 3 | The official repository for imv is now hosted on sr.ht. Please direct all patches and bug reports there. This GitHub repository is no longer updated. 4 | 5 | - [Project page](https://sr.ht/~exec64/imv/) 6 | - [Git repo](https://git.sr.ht/~exec64/imv/) 7 | - [Mailing list](https://sr.ht/~exec64/imv/lists) 8 | - [Bug tracker](https://todo.sr.ht/~exec64/imv) 9 | -------------------------------------------------------------------------------- /subprojects/inih.wrap: -------------------------------------------------------------------------------- 1 | [wrap-file] 2 | directory = inih-r52 3 | source_url = https://github.com/benhoyt/inih/archive/r52.tar.gz 4 | source_filename = r52.tar.gz 5 | source_hash = 439cff9ce9a8afc52d08772ac3e93b3cecd79c7707f871fb4534fb3a48201880 6 | patch_url = https://wrapdb.mesonbuild.com/v1/projects/inih/r52/1/get_zip 7 | patch_filename = inih-r52-1-wrap.zip 8 | patch_hash = b37abc23f1b558d1d28d47fc0c81295950e4fa2f236de35c41c505dccc422f7a 9 | -------------------------------------------------------------------------------- /src/bitmap.h: -------------------------------------------------------------------------------- 1 | #ifndef IMV_BITMAP_H 2 | #define IMV_BITMAP_H 3 | 4 | enum imv_pixelformat { 5 | IMV_ARGB, 6 | IMV_ABGR, 7 | }; 8 | 9 | struct imv_bitmap { 10 | int width; 11 | int height; 12 | enum imv_pixelformat format; 13 | unsigned char *data; 14 | }; 15 | 16 | /* Copy an imv_bitmap */ 17 | struct imv_bitmap *imv_bitmap_clone(struct imv_bitmap *bmp); 18 | 19 | /* Clean up a bitmap */ 20 | void imv_bitmap_free(struct imv_bitmap *bmp); 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Author and maintainer of imv: 2 | * Harry Jeffery 3 | 4 | People who have contributed to imv: 5 | * Carlo Abelli 6 | * Guillaume Brogi 7 | * Dmitrij D. Czarkoff 8 | * Jose Diez 9 | * Kenneth Hanley 10 | * Julian Heinzel 11 | * Hannes Körber 12 | * Aleksandra Kosiacka 13 | * Michal Koutenský 14 | * Sebastian Parborg 15 | 16 | --- 17 | When submitting a patch or pull request to imv, feel free to add yourself to 18 | this list, in alphabetical surname order. 19 | -------------------------------------------------------------------------------- /src/imv.h: -------------------------------------------------------------------------------- 1 | #ifndef IMV_H 2 | #define IMV_H 3 | 4 | #include 5 | 6 | struct imv; 7 | struct imv_backend; 8 | 9 | struct imv *imv_create(void); 10 | void imv_free(struct imv *imv); 11 | 12 | void imv_install_backend(struct imv *imv, const struct imv_backend *backend); 13 | 14 | bool imv_load_config(struct imv *imv); 15 | bool imv_parse_args(struct imv *imv, int argc, char **argv); 16 | 17 | void imv_add_path(struct imv *imv, const char *path); 18 | 19 | int imv_run(struct imv *imv); 20 | 21 | #endif 22 | 23 | /* vim:set ts=2 sts=2 sw=2 et: */ 24 | -------------------------------------------------------------------------------- /src/bitmap.c: -------------------------------------------------------------------------------- 1 | #include "bitmap.h" 2 | 3 | #include 4 | #include 5 | 6 | struct imv_bitmap *imv_bitmap_clone(struct imv_bitmap *bmp) 7 | { 8 | struct imv_bitmap *copy = malloc(sizeof *copy); 9 | const size_t num_bytes = 4 * bmp->width * bmp->height; 10 | copy->width = bmp->width; 11 | copy->height = bmp->height; 12 | copy->format = bmp->format; 13 | copy->data = malloc(num_bytes); 14 | memcpy(copy->data, bmp->data, num_bytes); 15 | return copy; 16 | } 17 | 18 | void imv_bitmap_free(struct imv_bitmap *bmp) 19 | { 20 | free(bmp->data); 21 | free(bmp); 22 | } 23 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | imv Contributing Guidelines 2 | =========================== 3 | 4 | 1) Please keep pull requests rebased onto the latest master. All merges should 5 | be simple fast-forward merges. While rebasing, tidy up and merge your commits 6 | as you think appropriate. 7 | 8 | 2) Please ensure that your changes fit with the existing coding style of imv. 9 | Strictly no tabs, and 2 spaces of indentation. 10 | 11 | 3) All contributions are provided under the terms of the MIT license. 12 | You retain the copyright ownership of any contributions you make. 13 | -------------------------------------------------------------------------------- /files/imv.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=imv 3 | GenericName=Image viewer 4 | GenericName[en_US]=Image viewer 5 | Comment=Fast freeimage-based Image Viewer 6 | Exec=imv %F 7 | NoDisplay=true 8 | Terminal=false 9 | Type=Application 10 | Categories=Graphics;2DGraphics;Viewer; 11 | MimeType=image/bmp;image/gif;image/jpeg;image/jpg;image/pjpeg;image/png;image/tiff;image/x-bmp;image/x-pcx;image/x-png;image/x-portable-anymap;image/x-portable-bitmap;image/x-portable-graymap;image/x-portable-pixmap;image/x-tga;image/x-xbitmap;image/heif 12 | Name[en_US]=imv 13 | Icon=multimedia-photo-viewer 14 | Keywords=photo;picture; 15 | -------------------------------------------------------------------------------- /src/log.h: -------------------------------------------------------------------------------- 1 | #ifndef IMV_LOG_H 2 | #define IMV_LOG_H 3 | 4 | enum imv_log_level { 5 | IMV_DEBUG, 6 | IMV_INFO, 7 | IMV_WARNING, 8 | IMV_ERROR 9 | }; 10 | 11 | /* Write to the log */ 12 | void imv_log(enum imv_log_level level, const char *fmt, ...); 13 | 14 | typedef void (*imv_log_callback)(enum imv_log_level level, const char *text, void *data); 15 | 16 | /* Subscribe to the log, callback is called whenever a log entry is written */ 17 | void imv_log_add_log_callback(imv_log_callback callback, void *data); 18 | 19 | /* Unsubscribe from the log */ 20 | void imv_log_remove_log_callback(imv_log_callback callback); 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /.builds/freebsd.yml: -------------------------------------------------------------------------------- 1 | image: freebsd/latest 2 | packages: 3 | - devel/icu 4 | - devel/meson 5 | - devel/pkgconf 6 | - graphics/freeimage 7 | - graphics/libGLU 8 | - graphics/libheif 9 | - graphics/libjpeg-turbo 10 | - graphics/libnsgif 11 | - graphics/librsvg2-rust 12 | - graphics/png 13 | - graphics/tiff 14 | - sysutils/cmocka 15 | - textproc/asciidoc 16 | - x11-toolkits/pango 17 | - x11/libxcb 18 | - x11/libxkbcommon 19 | sources: 20 | - https://git.sr.ht/~exec64/imv 21 | tasks: 22 | - meson: | 23 | meson imv build -D auto_features=enabled -D c_link_args='-L/usr/local/lib' 24 | ninja -C build test 25 | -------------------------------------------------------------------------------- /files/imv-dir.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=imv-dir 3 | Name[en_US]=imv 4 | GenericName=Image viewer 5 | GenericName[en_US]=Image viewer 6 | Comment=Fast freeimage-based Image Viewer | Open all images in a directory 7 | Exec=imv-dir %f 8 | NoDisplay=true 9 | Terminal=false 10 | Type=Application 11 | Categories=Graphics;2DGraphics;Viewer; 12 | MimeType=image/bmp;image/gif;image/jpeg;image/jpg;image/pjpeg;image/png;image/tiff;image/x-bmp;image/x-pcx;image/x-png;image/x-portable-anymap;image/x-portable-bitmap;image/x-portable-graymap;image/x-portable-pixmap;image/x-tga;image/x-xbitmap; 13 | Icon=multimedia-photo-viewer 14 | Keywords=photo;picture; 15 | -------------------------------------------------------------------------------- /.builds/archlinux.yml: -------------------------------------------------------------------------------- 1 | image: archlinux 2 | packages: 3 | - asciidoc 4 | - clang 5 | - cmocka 6 | - freeimage 7 | - glu 8 | - icu 9 | - libglvnd 10 | - libheif 11 | - libinih 12 | - libjpeg-turbo 13 | - libnsgif 14 | - libpng 15 | - librsvg 16 | - libtiff 17 | - libx11 18 | - libxcb 19 | - libxkbcommon 20 | - libxkbcommon-x11 21 | - meson 22 | - pango 23 | - wayland 24 | sources: 25 | - https://git.sr.ht/~exec64/imv 26 | tasks: 27 | - gcc: | 28 | CC=gcc meson imv build_gcc -D auto_features=enabled 29 | CC=gcc ninja -C build_gcc test 30 | - clang: | 31 | CC=clang meson imv build_clang -D auto_features=enabled 32 | CC=clang ninja -C build_clang test 33 | -------------------------------------------------------------------------------- /src/image.h: -------------------------------------------------------------------------------- 1 | #ifndef IMV_IMAGE_H 2 | #define IMV_IMAGE_H 3 | 4 | #include "bitmap.h" 5 | 6 | #ifdef IMV_BACKEND_LIBRSVG 7 | #include 8 | #endif 9 | 10 | struct imv_image; 11 | 12 | struct imv_image *imv_image_create_from_bitmap(struct imv_bitmap *bmp); 13 | 14 | #ifdef IMV_BACKEND_LIBRSVG 15 | struct imv_image *imv_image_create_from_svg(RsvgHandle *handle); 16 | #endif 17 | 18 | /* Cleans up an imv_image instance */ 19 | void imv_image_free(struct imv_image *image); 20 | 21 | /* Get the image width */ 22 | int imv_image_width(const struct imv_image *image); 23 | 24 | /* Get the image height */ 25 | int imv_image_height(const struct imv_image *image); 26 | 27 | #endif 28 | 29 | 30 | /* vim:set ts=2 sts=2 sw=2 et: */ 31 | -------------------------------------------------------------------------------- /doc/imv-msg.1.txt: -------------------------------------------------------------------------------- 1 | ///// 2 | vim:set ts=4 sw=4 tw=82 noet: 3 | ///// 4 | :quotes.~: 5 | 6 | imv-msg (1) 7 | =========== 8 | 9 | Name 10 | ---- 11 | imv-msg - Utility for sending commands to a running imv instance 12 | 13 | Description 14 | ----------- 15 | 16 | imv-msg is a tool to simplify the sending of commands to a running instance 17 | of imv. Given an instance's pid it opens the corresponding unix socket and 18 | sends the provided command. 19 | 20 | Synopsis 21 | -------- 22 | 'imv-msg' 23 | 24 | Authors 25 | ------- 26 | 27 | imv-msg is written and maintained by Harry Jeffery 28 | 29 | Full source code and other information can be found at 30 | . 31 | 32 | See Also 33 | -------- 34 | 35 | **imv**(1) 36 | -------------------------------------------------------------------------------- /doc/imv-dir.1.txt: -------------------------------------------------------------------------------- 1 | ///// 2 | vim:set ts=4 sw=4 tw=82 noet: 3 | ///// 4 | :quotes.~: 5 | 6 | imv (1) 7 | ======= 8 | 9 | Name 10 | ---- 11 | imv-dir - Open 'imv' for all images in a directory 12 | 13 | Description 14 | ----------- 15 | 16 | 'imv-dir' is a wrapper for 'imv' that auto-selects the directory where the image is located, so that the *next* and *previous* commands function in the same way as other image viewers. 17 | 18 | Note that this wrapper supports only a single image. For opening multiple images, use plain `imv`. 19 | 20 | Synopsis 21 | -------- 22 | 'imv-dir' path 23 | 24 | Authors 25 | ------- 26 | 27 | imv is written and maintained by Harry Jeffery 28 | with contributions from other developers. 29 | 30 | Full source code and other information can be found at 31 | . 32 | 33 | See Also 34 | -------- 35 | 36 | **imv**(1) 37 | -------------------------------------------------------------------------------- /.builds/ubuntu.yml: -------------------------------------------------------------------------------- 1 | image: ubuntu/lts 2 | packages: 3 | - asciidoc 4 | - clang 5 | - libcmocka-dev 6 | - libegl1-mesa-dev 7 | - libfreeimage-dev 8 | - libglu-dev 9 | - libheif-dev 10 | - libicu-dev 11 | - libinih-dev 12 | - libpango1.0-dev 13 | - libpng-dev 14 | - librsvg2-dev 15 | - libtiff-dev 16 | - libturbojpeg-dev 17 | - libwayland-dev 18 | - libx11-dev 19 | - libxcb1-dev 20 | - libxkbcommon-dev 21 | - libxkbcommon-x11-dev 22 | - mesa-common-dev 23 | - meson 24 | sources: 25 | - https://git.sr.ht/~exec64/imv 26 | tasks: 27 | # libnsgif isn't packaged by ubuntu 28 | - gcc: | 29 | CC=gcc meson imv build_gcc -D auto_features=enabled -D libnsgif=disabled 30 | CC=gcc ninja -C build_gcc test 31 | - clang: | 32 | CC=clang meson imv build_clang -D auto_features=enabled -D libnsgif=disabled 33 | CC=clang ninja -C build_clang test 34 | -------------------------------------------------------------------------------- /.builds/fedora.yml: -------------------------------------------------------------------------------- 1 | image: fedora/latest 2 | packages: 3 | - asciidoc 4 | - clang 5 | - freeimage-devel 6 | - inih 7 | - libX11-devel 8 | - libcmocka-devel 9 | - libicu-devel 10 | - libpng-devel 11 | - librsvg2-devel 12 | - libtiff-devel 13 | - libxcb-devel 14 | - libxkbcommon-devel 15 | - libxkbcommon-x11-devel 16 | - mesa-libEGL-devel 17 | - mesa-libGL-devel 18 | - mesa-libGLU-devel 19 | - meson 20 | - pango-devel 21 | - turbojpeg-devel 22 | - wayland-devel 23 | sources: 24 | - https://git.sr.ht/~exec64/imv 25 | tasks: 26 | # libheif ins't packaged by fedora 27 | # libnsgif isn't packaged by fedora 28 | - gcc: | 29 | CC=gcc meson imv build_gcc -D auto_features=enabled -D libheif=disabled -D libnsgif=disabled 30 | CC=gcc ninja -C build_gcc test 31 | - clang: | 32 | CC=clang meson imv build_clang -D auto_features=enabled -D libheif=disabled -D libnsgif=disabled 33 | CC=clang ninja -C build_clang test 34 | -------------------------------------------------------------------------------- /src/ipc.h: -------------------------------------------------------------------------------- 1 | #ifndef IMV_IPC_H 2 | #define IMV_IPC_H 3 | 4 | #include 5 | 6 | /* imv_ipc provides a listener on a unix socket that listens for commands. 7 | * When a command is received, a callback function is called. 8 | */ 9 | struct imv_ipc; 10 | 11 | /* Creates an imv_ipc instance */ 12 | struct imv_ipc *imv_ipc_create(void); 13 | 14 | /* Cleans up an imv_ipc instance */ 15 | void imv_ipc_free(struct imv_ipc *ipc); 16 | 17 | typedef void (*imv_ipc_callback)(const char *command, void *data); 18 | 19 | /* When a command is received, imv_ipc will call the callback function passed 20 | * in. Only one callback function at a time can be connected. The data argument 21 | * is passed back to the callback to allow for context passing 22 | */ 23 | void imv_ipc_set_command_callback(struct imv_ipc *ipc, 24 | imv_ipc_callback callback, void *data); 25 | 26 | /* Given a pid, emits the path of the unix socket that would connect to an imv 27 | * instance with that pid 28 | */ 29 | void imv_ipc_path(char *buf, size_t len, int pid); 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /src/imv_msg.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "ipc.h" 9 | 10 | int main(int argc, char **argv) 11 | { 12 | if (argc < 3) { 13 | fprintf(stderr, "Usage: %s \n", argv[0]); 14 | return 0; 15 | } 16 | 17 | int sockfd = socket(AF_UNIX, SOCK_STREAM, 0); 18 | assert(sockfd); 19 | 20 | struct sockaddr_un desc = { 21 | .sun_family = AF_UNIX 22 | }; 23 | imv_ipc_path(desc.sun_path, sizeof desc.sun_path, atoi(argv[1])); 24 | 25 | if (connect(sockfd, (struct sockaddr *)&desc, sizeof desc) < 0) { 26 | perror("Failed to connect"); 27 | return 1; 28 | } 29 | 30 | char buf[4096] = {0}; 31 | for (int i = 2; i < argc; ++i) { 32 | strncat(buf, argv[i], sizeof buf - 1); 33 | if (i + 1 < argc) { 34 | strncat(buf, " ", sizeof buf - 1); 35 | } 36 | } 37 | strncat(buf, "\n", sizeof buf - 1); 38 | 39 | write(sockfd, buf, strlen(buf)); 40 | close(sockfd); 41 | return 0; 42 | } 43 | -------------------------------------------------------------------------------- /.builds/debian.yml: -------------------------------------------------------------------------------- 1 | image: debian/stable 2 | packages: 3 | - asciidoc 4 | - clang 5 | - libcmocka-dev 6 | - libegl1-mesa-dev 7 | - libfreeimage-dev 8 | - libglu-dev 9 | - libheif-dev 10 | - libicu-dev 11 | - libinih-dev 12 | - libpango1.0-dev 13 | - libpng-dev 14 | - librsvg2-dev 15 | - libtiff-dev 16 | - libturbojpeg-dev 17 | - libwayland-dev 18 | - libx11-dev 19 | - libxcb1-dev 20 | - libxkbcommon-dev 21 | - libxkbcommon-x11-dev 22 | - mesa-common-dev 23 | - meson 24 | sources: 25 | - https://git.sr.ht/~exec64/imv 26 | tasks: 27 | - dummy: | 28 | true 29 | #FIXME: 30 | # the old meson package in debian has a bug and crashes with 31 | # our meson.build; re-enable this once debian packages a version 32 | # newer than 0.49.2 33 | # libnsgif isn't packaged by debian 34 | # - gcc: | 35 | # CC=gcc meson imv build_gcc -D auto_features=enabled -D libnsgif=disabled 36 | # CC=gcc ninja -C build_gcc test 37 | # - clang: | 38 | # CC=clang meson imv build_clang -D auto_features=enabled -D libnsgif=disabled 39 | # CC=clang ninja -C build_clang test 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Harry Jeffery 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/commands.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMANDS_H 2 | #define COMMANDS_H 3 | 4 | struct list; 5 | struct imv_commands; 6 | 7 | /* Create an imv_commands instance */ 8 | struct imv_commands *imv_commands_create(void); 9 | 10 | /* Cleans up an imv_commands instance */ 11 | void imv_commands_free(struct imv_commands *cmds); 12 | 13 | /* Register a new command. When a command is executed, the appropriate handler 14 | * is called, with a void* for passing context. 15 | */ 16 | void imv_command_register(struct imv_commands *cmds, const char *command, 17 | void (*handler)(struct list*, const char*, void*)); 18 | 19 | /* Add a command alias. Any arguments provided when invoking an alias are 20 | * appended to the arguments being passed to the command. 21 | */ 22 | void imv_command_alias(struct imv_commands *cmds, const char *command, const char *alias); 23 | 24 | /* Execute a single command */ 25 | int imv_command_exec(struct imv_commands *cmds, const char *command, void *data); 26 | 27 | /* Execute a list of commands */ 28 | int imv_command_exec_list(struct imv_commands *cmds, struct list *commands, void *data); 29 | 30 | #endif 31 | 32 | /* vim:set ts=2 sts=2 sw=2 et: */ 33 | -------------------------------------------------------------------------------- /files/imv_config: -------------------------------------------------------------------------------- 1 | # Default config for imv 2 | 3 | [options] 4 | 5 | # Suppress built-in key bindings, and specify them explicitly in this 6 | # config file. 7 | suppress_default_binds = true 8 | 9 | [aliases] 10 | # Define aliases here. Any arguments passed to an alias are appended to the 11 | # command. 12 | # alias = command to run 13 | 14 | [binds] 15 | # Define some key bindings 16 | q = quit 17 | y = exec echo working! 18 | 19 | # Image navigation 20 | = prev 21 | = prev 22 | = next 23 | = next 24 | gg = goto 1 25 | = goto -1 26 | 27 | # Panning 28 | j = pan 0 -50 29 | k = pan 0 50 30 | h = pan 50 0 31 | l = pan -50 0 32 | 33 | # Zooming 34 | = zoom 1 35 | = zoom 1 36 | i = zoom 1 37 | = zoom -1 38 | = zoom -1 39 | o = zoom -1 40 | 41 | # Rotate Clockwise by 90 degrees 42 | = rotate by 90 43 | 44 | # Other commands 45 | x = close 46 | f = fullscreen 47 | d = overlay 48 | p = exec echo $imv_current_file 49 | c = center 50 | s = scaling next 51 | = upscaling next 52 | a = zoom actual 53 | r = reset 54 | 55 | # Gif playback 56 | = next_frame 57 | = toggle_playing 58 | 59 | # Slideshow control 60 | t = slideshow +1 61 | = slideshow -1 62 | -------------------------------------------------------------------------------- /src/source_private.h: -------------------------------------------------------------------------------- 1 | #ifndef IMV_SOURCE_PRIVATE_H 2 | #define IMV_SOURCE_PRIVATE_H 3 | 4 | struct imv_image; 5 | struct imv_source; 6 | 7 | /* This is the interface a source needs to implement to function correctly. 8 | * Backends act as a "factory" for sources by calling imv_source_create 9 | * with a pointer to a static vtable, and a pointer to that implementation's 10 | * private data, pre-initialised. 11 | */ 12 | 13 | struct imv_source_vtable { 14 | 15 | /* Loads the first frame, if successful puts output in image and duration 16 | * (in milliseconds) in frametime. If unsuccessful, image shall be NULL. A 17 | * still image should use a frametime of 0. 18 | */ 19 | void (*load_first_frame)(void *private, struct imv_image **image, int *frametime); 20 | 21 | /* Loads the next frame, if successful puts output in image and duration 22 | * (in milliseconds) in frametime. If unsuccessful, image shall be NULL. 23 | */ 24 | void (*load_next_frame)(void *private, struct imv_image **image, int *frametime); 25 | 26 | /* Cleans up the private data of a source */ 27 | void (*free)(void *private); 28 | }; 29 | 30 | /* Build a source given its vtable and a pointer to the private data */ 31 | struct imv_source *imv_source_create(const struct imv_source_vtable *vt, void *private); 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /src/binds.h: -------------------------------------------------------------------------------- 1 | #ifndef IMV_BINDS_H 2 | #define IMV_BINDS_H 3 | 4 | #include 5 | 6 | struct imv_binds; 7 | struct list; 8 | 9 | enum bind_result { 10 | BIND_SUCCESS, 11 | BIND_INVALID_KEYS, 12 | BIND_INVALID_COMMAND, 13 | BIND_CONFLICTS, 14 | }; 15 | 16 | /* Create an imv_binds instance */ 17 | struct imv_binds *imv_binds_create(void); 18 | 19 | /* Clean up an imv_binds instance */ 20 | void imv_binds_free(struct imv_binds *binds); 21 | 22 | /* Create a key binding */ 23 | enum bind_result imv_binds_add(struct imv_binds *binds, const struct list *keys, const char *cmd); 24 | 25 | /* Remove all key bindings */ 26 | void imv_binds_clear(struct imv_binds *binds); 27 | 28 | /* Clear all bindings for a key*/ 29 | void imv_binds_clear_key(struct imv_binds *binds, const struct list *keys); 30 | 31 | /* Fetch the list of keys pressed so far */ 32 | const struct list *imv_bind_input_buffer(struct imv_binds *binds); 33 | 34 | /* Abort the current input key sequence */ 35 | void imv_bind_clear_input(struct imv_binds *binds); 36 | 37 | /* Handle an input event, if a bind is triggered, return its command */ 38 | struct list *imv_bind_handle_event(struct imv_binds *binds, const char *event); 39 | 40 | /* Convert a string (such as from a config) to a key list */ 41 | struct list *imv_bind_parse_keys(const char *keys); 42 | 43 | /* Convert a key list to a string */ 44 | size_t imv_bind_print_keylist(const struct list *keys, char *buf, size_t len); 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /test/list.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "list.h" 7 | 8 | static void test_split_string(void **state) 9 | { 10 | (void)state; 11 | 12 | struct list *list; 13 | 14 | list = list_from_string("word", ' '); 15 | assert_true(list); 16 | assert_true(list->len == 1); 17 | assert_false(strcmp(list->items[0], "word")); 18 | list_deep_free(list); 19 | 20 | list = list_from_string("hello world this is a test", ' '); 21 | assert_true(list); 22 | assert_true(list->len == 6); 23 | assert_false(strcmp(list->items[0], "hello")); 24 | assert_false(strcmp(list->items[1], "world")); 25 | assert_false(strcmp(list->items[2], "this")); 26 | assert_false(strcmp(list->items[3], "is")); 27 | assert_false(strcmp(list->items[4], "a")); 28 | assert_false(strcmp(list->items[5], "test")); 29 | list_deep_free(list); 30 | 31 | list = list_from_string(" odd whitespace test ", ' '); 32 | assert_true(list); 33 | assert_true(list->len == 3); 34 | assert_false(strcmp(list->items[0], "odd")); 35 | assert_false(strcmp(list->items[1], "whitespace")); 36 | assert_false(strcmp(list->items[2], "test")); 37 | list_deep_free(list); 38 | } 39 | 40 | int main(void) 41 | { 42 | const struct CMUnitTest tests[] = { 43 | cmocka_unit_test(test_split_string), 44 | }; 45 | 46 | return cmocka_run_group_tests(tests, NULL, NULL); 47 | } 48 | 49 | 50 | /* vim:set ts=2 sts=2 sw=2 et: */ 51 | -------------------------------------------------------------------------------- /src/keyboard.h: -------------------------------------------------------------------------------- 1 | #ifndef IMV_KEYBOARD_H 2 | #define IMV_KEYBOARD_H 3 | 4 | #include 5 | #include 6 | 7 | struct imv_keyboard; 8 | 9 | /* Create a keyboard instance */ 10 | struct imv_keyboard *imv_keyboard_create(void); 11 | 12 | /* Clean up a keyboard */ 13 | void imv_keyboard_free(struct imv_keyboard *keyboard); 14 | 15 | /* Notify the keyboard of the state of a key */ 16 | void imv_keyboard_update_key(struct imv_keyboard *keyboard, int scancode, bool pressed); 17 | 18 | /* Notify the keyboard of the state of the modifiers */ 19 | void imv_keyboard_update_mods(struct imv_keyboard *keyboard, 20 | int depressed, int latched, int locked); 21 | 22 | /* Write the null-terminated name of the key corresponding to scancode into buf */ 23 | size_t imv_keyboard_keyname(struct imv_keyboard *keyboard, int scancode, char *buf, size_t buflen); 24 | 25 | /* Describe the key corresponding to scancode, with modifier keys prefixed */ 26 | char *imv_keyboard_describe_key(struct imv_keyboard *keyboard, int scancode); 27 | 28 | /* Write the null-terminated text generated by scancode being pressed into buf */ 29 | size_t imv_keyboard_get_text(struct imv_keyboard *keyboard, int scancode, char *buf, size_t buflen); 30 | 31 | /* Initialise the keymap from a string containing the description */ 32 | void imv_keyboard_set_keymap(struct imv_keyboard *keyboard, const char *keymap); 33 | 34 | /* Should the key on a given scancode repeat when held down */ 35 | bool imv_keyboard_should_key_repeat(struct imv_keyboard *keyboard, int scancode); 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include "imv.h" 2 | 3 | struct imv_backend; 4 | 5 | extern const struct imv_backend imv_backend_freeimage; 6 | extern const struct imv_backend imv_backend_libpng; 7 | extern const struct imv_backend imv_backend_librsvg; 8 | extern const struct imv_backend imv_backend_libtiff; 9 | extern const struct imv_backend imv_backend_libjpeg; 10 | extern const struct imv_backend imv_backend_libnsgif; 11 | extern const struct imv_backend imv_backend_libheif; 12 | 13 | int main(int argc, char **argv) 14 | { 15 | struct imv *imv = imv_create(); 16 | 17 | if (!imv) { 18 | return 1; 19 | } 20 | 21 | #ifdef IMV_BACKEND_FREEIMAGE 22 | imv_install_backend(imv, &imv_backend_freeimage); 23 | #endif 24 | 25 | #ifdef IMV_BACKEND_LIBTIFF 26 | imv_install_backend(imv, &imv_backend_libtiff); 27 | #endif 28 | 29 | #ifdef IMV_BACKEND_LIBPNG 30 | imv_install_backend(imv, &imv_backend_libpng); 31 | #endif 32 | 33 | #ifdef IMV_BACKEND_LIBJPEG 34 | imv_install_backend(imv, &imv_backend_libjpeg); 35 | #endif 36 | 37 | #ifdef IMV_BACKEND_LIBRSVG 38 | imv_install_backend(imv, &imv_backend_librsvg); 39 | #endif 40 | 41 | #ifdef IMV_BACKEND_LIBNSGIF 42 | imv_install_backend(imv, &imv_backend_libnsgif); 43 | #endif 44 | 45 | #ifdef IMV_BACKEND_LIBHEIF 46 | imv_install_backend(imv, &imv_backend_libheif); 47 | #endif 48 | 49 | if (!imv_load_config(imv)) { 50 | imv_free(imv); 51 | return 1; 52 | } 53 | 54 | if (!imv_parse_args(imv, argc, argv)) { 55 | imv_free(imv); 56 | return 1; 57 | } 58 | 59 | int ret = imv_run(imv); 60 | 61 | imv_free(imv); 62 | 63 | return ret; 64 | } 65 | 66 | /* vim:set ts=2 sts=2 sw=2 et: */ 67 | -------------------------------------------------------------------------------- /src/backend.h: -------------------------------------------------------------------------------- 1 | #ifndef IMV_BACKEND_H 2 | #define IMV_BACKEND_H 3 | 4 | #include 5 | 6 | struct imv_source; 7 | 8 | enum backend_result { 9 | 10 | /* The backend recognises the file's data and thinks it can load it */ 11 | BACKEND_SUCCESS = 0, 12 | 13 | /* Represents a bad file or path, implies that other backends would also fail 14 | * and shouldn't be tried. 15 | */ 16 | BACKEND_BAD_PATH = 1, 17 | 18 | /* There's data, but this backend doesn't understand it. */ 19 | BACKEND_UNSUPPORTED = 2, 20 | }; 21 | 22 | /* A backend is responsible for taking a path, or a raw data pointer, and 23 | * converting that into an imv_source. Each backend may be powered by a 24 | * different image library and support different image formats. 25 | */ 26 | struct imv_backend { 27 | 28 | /* Name of the backend, for debug and user informational purposes */ 29 | const char *name; 30 | 31 | /* Information about the backend, displayed by help dialog */ 32 | const char *description; 33 | 34 | /* Official website address */ 35 | const char *website; 36 | 37 | /* License the backend is used under */ 38 | const char *license; 39 | 40 | /* Tries to open the given path. If successful, BACKEND_SUCCESS is returned 41 | * and src will point to an imv_source instance for the given path. 42 | */ 43 | enum backend_result (*open_path)(const char *path, struct imv_source **src); 44 | 45 | /* Tries to read the passed data. If successful, BACKEND_SUCCESS is returned 46 | * and src will point to an imv_source instance for the given data. 47 | */ 48 | enum backend_result (*open_memory)(void *data, size_t len, struct imv_source **src); 49 | }; 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /src/log.c: -------------------------------------------------------------------------------- 1 | #include "log.h" 2 | 3 | #include "list.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | static struct list *g_log_clients = NULL; 11 | static char g_log_buffer[16384]; 12 | 13 | struct log_client { 14 | imv_log_callback callback; 15 | void *data; 16 | }; 17 | 18 | void imv_log(enum imv_log_level level, const char *fmt, ...) 19 | { 20 | assert(fmt); 21 | 22 | /* Exit early if no one's listening */ 23 | if (!g_log_clients || !g_log_clients->len) { 24 | return; 25 | } 26 | 27 | va_list args; 28 | va_start(args, fmt); 29 | vsnprintf(g_log_buffer, sizeof g_log_buffer, fmt, args); 30 | va_end(args); 31 | 32 | for (size_t i = 0; i < g_log_clients->len; ++i) { 33 | struct log_client *client = g_log_clients->items[i]; 34 | client->callback(level, g_log_buffer, client->data); 35 | } 36 | } 37 | 38 | void imv_log_add_log_callback(imv_log_callback callback, void *data) 39 | { 40 | assert(callback); 41 | 42 | struct log_client *client = calloc(1, sizeof *client); 43 | client->callback = callback; 44 | client->data = data; 45 | 46 | if (!g_log_clients) { 47 | g_log_clients = list_create(); 48 | } 49 | 50 | list_append(g_log_clients, client); 51 | } 52 | 53 | void imv_log_remove_log_callback(imv_log_callback callback) 54 | { 55 | assert(callback); 56 | 57 | if (!callback || !g_log_clients) { 58 | return; 59 | } 60 | 61 | for (size_t i = 0; i < g_log_clients->len; ++i) { 62 | struct log_client *client = g_log_clients->items[i]; 63 | if (client->callback == callback) { 64 | free(client); 65 | list_remove(g_log_clients, i); 66 | return; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/source.h: -------------------------------------------------------------------------------- 1 | #ifndef IMV_SOURCE_H 2 | #define IMV_SOURCE_H 3 | 4 | /* While imv_image represents a single frame of an image, be it a bitmap or 5 | * vector image, imv_source represents an open handle to an image file, which 6 | * can emit one or more imv_images. 7 | */ 8 | struct imv_source; 9 | 10 | struct imv_source_message; 11 | struct imv_image; 12 | 13 | /* Clean up a source. Blocks if the source is active in the background. Async 14 | * version does not block, performing cleanup in another thread */ 15 | void imv_source_async_free(struct imv_source *src); 16 | void imv_source_free(struct imv_source *src); 17 | 18 | /* Load the first frame. Silently aborts if source is already loading. Async 19 | * version performs loading in background. */ 20 | void imv_source_async_load_first_frame(struct imv_source *src); 21 | void imv_source_load_first_frame(struct imv_source *src); 22 | 23 | /* Load the next frame. Silently aborts if source is already loading. Async 24 | * version performs loading in background. */ 25 | void imv_source_async_load_next_frame(struct imv_source *src); 26 | void imv_source_load_next_frame(struct imv_source *src); 27 | 28 | typedef void (*imv_source_callback)(struct imv_source_message *message); 29 | 30 | /* Sets the callback function to be called when frame loading completes */ 31 | void imv_source_set_callback(struct imv_source *src, imv_source_callback callback, void *data); 32 | 33 | struct imv_source_message { 34 | /* Pointer to sender of message */ 35 | struct imv_source *source; 36 | 37 | /* User-supplied pointer */ 38 | void *user_data; 39 | 40 | /* Receiver is responsible for destroying image */ 41 | struct imv_image *image; 42 | 43 | /* If an animated gif, the frame's duration in milliseconds, else 0 */ 44 | int frametime; 45 | }; 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /src/image.c: -------------------------------------------------------------------------------- 1 | #include "image.h" 2 | 3 | #include "bitmap.h" 4 | 5 | #include 6 | 7 | struct imv_image { 8 | int width; 9 | int height; 10 | struct imv_bitmap *bitmap; 11 | #ifdef IMV_BACKEND_LIBRSVG 12 | RsvgHandle *svg; 13 | #endif 14 | }; 15 | 16 | struct imv_image *imv_image_create_from_bitmap(struct imv_bitmap *bmp) 17 | { 18 | struct imv_image *image = calloc(1, sizeof *image); 19 | image->width = bmp->width; 20 | image->height = bmp->height; 21 | image->bitmap = bmp; 22 | return image; 23 | } 24 | 25 | #ifdef IMV_BACKEND_LIBRSVG 26 | struct imv_image *imv_image_create_from_svg(RsvgHandle *handle) 27 | { 28 | struct imv_image *image = calloc(1, sizeof *image); 29 | image->svg = handle; 30 | 31 | RsvgDimensionData dim; 32 | rsvg_handle_get_dimensions(handle, &dim); 33 | image->width = dim.width; 34 | image->height = dim.height; 35 | return image; 36 | } 37 | #endif 38 | 39 | void imv_image_free(struct imv_image *image) 40 | { 41 | if (!image) { 42 | return; 43 | } 44 | 45 | if (image->bitmap) { 46 | imv_bitmap_free(image->bitmap); 47 | } 48 | 49 | #ifdef IMV_BACKEND_LIBRSVG 50 | if (image->svg) { 51 | g_object_unref(image->svg); 52 | } 53 | #endif 54 | 55 | free(image); 56 | } 57 | 58 | int imv_image_width(const struct imv_image *image) 59 | { 60 | return image ? image->width : 0; 61 | } 62 | 63 | int imv_image_height(const struct imv_image *image) 64 | { 65 | return image ? image->height : 0; 66 | } 67 | 68 | /* Non-public functions, only used by imv_canvas */ 69 | struct imv_bitmap *imv_image_get_bitmap(const struct imv_image *image) 70 | { 71 | return image->bitmap; 72 | } 73 | 74 | #ifdef IMV_BACKEND_LIBRSVG 75 | RsvgHandle *imv_image_get_svg(const struct imv_image *image) 76 | { 77 | return image->svg; 78 | } 79 | #endif 80 | 81 | /* vim:set ts=2 sts=2 sw=2 et: */ 82 | -------------------------------------------------------------------------------- /PACKAGERS.md: -------------------------------------------------------------------------------- 1 | # Dear Packager, 2 | 3 | This document is a quick summary of all you need to know to package imv for 4 | your favourite operating system. 5 | 6 | ## Configuring `imv` 7 | 8 | ### 1. Select window systems to support 9 | 10 | Your options here are Wayland or X11, or both. By default both are included, 11 | with a separate binary for each being built. `/usr/bin/imv` will be a script 12 | that checks for a Wayland compositor before running the appropriate binary. 13 | 14 | If you only care about one of these, you can specify to build only one of these 15 | by passing `-D windows=wayland` to `meson`, in which case only that 16 | binary shall be packaged without the need for a launcher script to 17 | select between the two. 18 | 19 | Alternatively, you could provide separate packages for X11 and Wayland that 20 | act as alternatives to each other. 21 | 22 | ### 2. Select backends to include 23 | 24 | imv supports multiple "backends" in a plugin style architecture. Each backend 25 | provides support for different image formats using different underlying 26 | libraries. The ones that are right for your operating system depend on which 27 | formats you need support for, and your licensing requirements. 28 | 29 | imv is published under the MIT license, but its backends may have different 30 | licensing requirements. 31 | 32 | By default, the libraries available are detected and support for them is 33 | automatically enabled, but you can enable or disable specific ones by 34 | passing `-D freeimage=enabled` or `-D libtiff=disabled`. 35 | You can also make sure all the backends are enabled by passing 36 | `-D auto_features=enabled`. 37 | 38 | ## Building `imv` 39 | 40 | Once your backends have been configured and you've confirmed the library 41 | each backend uses is installed, you can simply follow the Installation section 42 | of the [README](README.md) to build imv. 43 | 44 | ## Packaging `imv` 45 | 46 | Package the resulting binary and man pages in your operating system's native 47 | package format. 48 | -------------------------------------------------------------------------------- /src/console.h: -------------------------------------------------------------------------------- 1 | #ifndef IMV_CONSOLE 2 | #define IMV_CONSOLE 3 | 4 | #include 5 | #include 6 | 7 | struct imv_console; 8 | 9 | /* Create a console instance */ 10 | struct imv_console *imv_console_create(void); 11 | 12 | /* Clean up a console */ 13 | void imv_console_free(struct imv_console *console); 14 | 15 | /* Set the callback to be invoked when a command to run by the console */ 16 | typedef void (*imv_console_callback)(const char *command, void *data); 17 | void imv_console_set_command_callback(struct imv_console *console, 18 | imv_console_callback callback, void *data); 19 | 20 | /* Returns true if console is still active (i.e. user hasn't hit enter/escape yet */ 21 | bool imv_console_is_active(struct imv_console *console); 22 | 23 | /* Mark console as activated until user exits or submits a command */ 24 | void imv_console_activate(struct imv_console *console); 25 | 26 | /* Pass text input to the console */ 27 | void imv_console_input(struct imv_console *console, const char *text); 28 | 29 | /* Pass a key input to the console. Returns true if consumed. If so, 30 | * do not also send input text to the console. 31 | */ 32 | bool imv_console_key(struct imv_console *console, const char *key); 33 | 34 | /* What is the console prompt's current text? */ 35 | const char *imv_console_prompt(struct imv_console *console); 36 | 37 | /* What is the console prompt's current cursor position? (bytes into UTF-8 string) */ 38 | size_t imv_console_prompt_cursor(struct imv_console *console); 39 | 40 | /* What is the output history of the console? */ 41 | const char *imv_console_backlog(struct imv_console *console); 42 | 43 | /* Write some text to the console's backlog */ 44 | void imv_console_write(struct imv_console *console, const char *text); 45 | 46 | /* Add a tab-completion template. If the users hits tab, the rest of the 47 | * command will be suggested. If multiple matches, tab will cycle through them. 48 | */ 49 | void imv_console_add_completion(struct imv_console *console, const char *template); 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /src/list.h: -------------------------------------------------------------------------------- 1 | #ifndef LIST_H 2 | #define LIST_H 3 | 4 | #include 5 | #include 6 | 7 | /* Generic list. You know what this is. */ 8 | struct list { 9 | size_t len; 10 | size_t cap; 11 | void **items; 12 | }; 13 | 14 | /* Create a list instance */ 15 | struct list *list_create(void); 16 | 17 | /* Clean up a list, caller should clean up contents of the list first */ 18 | void list_free(struct list *list); 19 | 20 | /* Clean up a list, calling free() on each item first */ 21 | void list_deep_free(struct list *list); 22 | 23 | /* Append an item to the list. Automatically resizes the list if needed */ 24 | void list_append(struct list *list, void *item); 25 | 26 | /* Grow the list's storage to a given size, useful for avoiding unnecessary 27 | * reallocations prior to inserting many items 28 | */ 29 | void list_grow(struct list *list, size_t min_size); 30 | 31 | /* Remove an item from the list at a given 0-based index */ 32 | void list_remove(struct list *list, size_t index); 33 | 34 | /* Insert an item into the list before the given index */ 35 | void list_insert(struct list *list, size_t index, void *item); 36 | 37 | /* Empty the list. Caller should clean up the contents of the list first */ 38 | void list_clear(struct list *list); 39 | 40 | /* Split a null-terminated string, separating by the given delimiter. 41 | * Multiple consecutive delimiters count as a single delimiter, so no empty 42 | * string list items are emitted 43 | */ 44 | struct list *list_from_string(const char *string, char delim); 45 | 46 | /* Returns the index of the first item to match key, determined by the cmp 47 | * function 48 | */ 49 | int list_find( 50 | struct list *list, 51 | int (*cmp)(const void *item, const void *key), 52 | const void *key 53 | ); 54 | 55 | /* Assumes all list items are null-terminated strings, and concatenates them 56 | * separated by sep, starting at the index given by start 57 | */ 58 | char *list_to_string(struct list *list, const char *sep, size_t start); 59 | 60 | #endif 61 | 62 | /* vim:set ts=2 sts=2 sw=2 et: */ 63 | -------------------------------------------------------------------------------- /src/dummy_window.c: -------------------------------------------------------------------------------- 1 | #include "window.h" 2 | 3 | #include 4 | #include 5 | 6 | struct imv_window *imv_window_create(int w, int h, const char *title) 7 | { 8 | (void)w; 9 | (void)h; 10 | (void)title; 11 | return NULL; 12 | } 13 | 14 | void imv_window_free(struct imv_window *window) 15 | { 16 | (void)window; 17 | } 18 | 19 | void imv_window_clear(struct imv_window *window, unsigned char r, 20 | unsigned char g, unsigned char b) 21 | { 22 | (void)window; 23 | (void)r; 24 | (void)g; 25 | (void)b; 26 | } 27 | 28 | void imv_window_get_size(struct imv_window *window, int *w, int *h) 29 | { 30 | (void)window; 31 | (void)w; 32 | (void)h; 33 | } 34 | 35 | void imv_window_get_framebuffer_size(struct imv_window *window, int *w, int *h) 36 | { 37 | (void)window; 38 | (void)w; 39 | (void)h; 40 | } 41 | 42 | void imv_window_set_title(struct imv_window *window, const char *title) 43 | { 44 | (void)window; 45 | (void)title; 46 | } 47 | 48 | bool imv_window_is_fullscreen(struct imv_window *window) 49 | { 50 | (void)window; 51 | return false; 52 | } 53 | 54 | void imv_window_set_fullscreen(struct imv_window *window, bool fullscreen) 55 | { 56 | (void)window; 57 | (void)fullscreen; 58 | } 59 | 60 | bool imv_window_get_mouse_button(struct imv_window *window, int button) 61 | { 62 | (void)window; 63 | (void)button; 64 | return false; 65 | } 66 | 67 | void imv_window_get_mouse_position(struct imv_window *window, double *x, double *y) 68 | { 69 | (void)window; 70 | (void)x; 71 | (void)y; 72 | } 73 | 74 | void imv_window_present(struct imv_window *window) 75 | { 76 | (void)window; 77 | } 78 | 79 | void imv_window_wait_for_event(struct imv_window *window, double timeout) 80 | { 81 | (void)window; 82 | (void)timeout; 83 | } 84 | 85 | void imv_window_push_event(struct imv_window *window, struct imv_event *e) 86 | { 87 | (void)window; 88 | (void)e; 89 | } 90 | 91 | void imv_window_pump_events(struct imv_window *window, imv_event_handler handler, void *data) 92 | { 93 | (void)window; 94 | (void)handler; 95 | (void)data; 96 | } 97 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | # all - Build both, determine which to use at runtime 2 | # wayland - Only provide Wayland support 3 | # x11 - Only provide X11 support 4 | option('windows', 5 | type : 'combo', 6 | value : 'all', 7 | choices : ['all', 'wayland', 'x11'], 8 | description : 'window system to use' 9 | ) 10 | 11 | option('test', 12 | type : 'feature', 13 | description : 'enable tests' 14 | ) 15 | 16 | option('contrib-commands', 17 | type: 'boolean', 18 | value: true, 19 | description: 'Install extra commands available in `contrib/`' 20 | ) 21 | 22 | option('man', 23 | type : 'feature', 24 | description : 'enable man pages' 25 | ) 26 | 27 | # Available backends: 28 | 29 | # FreeImage http://freeimage.sourceforge.net 30 | # depends: libjpeg, openexr, openjpeg2, libwebp, libraw, jxrlib 31 | # license: FIPL v1.0 32 | option('freeimage', 33 | type : 'feature', 34 | description : 'provides: png, jpg, animated gif, raw, psd, bmp, tiff, webp, etc.' 35 | ) 36 | 37 | # libtiff 38 | # depends: libjpeg zlib xz zstd 39 | # license: MIT 40 | option('libtiff', 41 | type : 'feature', 42 | description : 'provides: tiff' 43 | ) 44 | 45 | # libpng http://www.libpng.org/pub/png/libpng.html 46 | # depends: zlib 47 | # license: libpng license 48 | option('libpng', 49 | type : 'feature', 50 | description : 'provides: png' 51 | ) 52 | 53 | # libjpeg-turbo https://libjpeg-turbo.org/ 54 | # depends: none 55 | # license: modified bsd 56 | option('libjpeg', 57 | type : 'feature', 58 | description : 'provides: jpeg' 59 | ) 60 | 61 | # librsvg https://wiki.gnome.org/Projects/LibRsvg 62 | # depends: gdk-pixbuf2 pango libcroco 63 | # license: LGPL 64 | option('librsvg', 65 | type : 'feature', 66 | description : 'provides: svg' 67 | ) 68 | 69 | # libnsgif https://www.netsurf-browser.org/projects/libnsgif/ 70 | # depends: none 71 | # license: MIT 72 | option('libnsgif', 73 | type : 'feature', 74 | description : 'provides: animated gif' 75 | ) 76 | 77 | # libheif http://www.libheif.org 78 | # depends: none 79 | # license: LGPL 80 | option('libheif', 81 | type : 'feature', 82 | description : 'provides: heif' 83 | ) 84 | -------------------------------------------------------------------------------- /src/navigator.h: -------------------------------------------------------------------------------- 1 | #ifndef IMV_NAVIGATOR_H 2 | #define IMV_NAVIGATOR_H 3 | 4 | #include 5 | 6 | /* Creates an instance of imv_navigator */ 7 | struct imv_navigator *imv_navigator_create(void); 8 | 9 | /* Cleans up an imv_navigator instance */ 10 | void imv_navigator_free(struct imv_navigator *nav); 11 | 12 | /* Adds the given path to the navigator's internal list. 13 | * If a directory is given, all files within that directory are added. 14 | * An internal copy of path is made. 15 | * If recursive is non-zero then subdirectories are recursed into. 16 | * Non-zero return code denotes failure. */ 17 | int imv_navigator_add(struct imv_navigator *nav, const char *path, 18 | int recursive); 19 | 20 | /* Returns a read-only reference to the current path. The pointer is only 21 | * guaranteed to be valid until the next call to an imv_navigator method. */ 22 | const char *imv_navigator_selection(struct imv_navigator *nav); 23 | 24 | /* Returns the index of the currently selected path */ 25 | size_t imv_navigator_index(struct imv_navigator *nav); 26 | 27 | /* Change the currently selected path. dir = -1 for previous, 1 for next. */ 28 | void imv_navigator_select_rel(struct imv_navigator *nav, ssize_t dir); 29 | 30 | /* Change the currently selected path. 0 = first, 1 = second, etc. */ 31 | void imv_navigator_select_abs(struct imv_navigator *nav, ssize_t index); 32 | 33 | /* Removes the given path. The current selection is updated if necessary, 34 | * based on the last direction the selection moved. */ 35 | void imv_navigator_remove(struct imv_navigator *nav, const char *path); 36 | 37 | /* Removes the given index. The current selection is updated if necessary, 38 | * based on the last direction the selection moved. */ 39 | void imv_navigator_remove_at(struct imv_navigator *nav, size_t index); 40 | 41 | /* Removes all paths */ 42 | void imv_navigator_remove_all(struct imv_navigator *nav); 43 | 44 | /* Return the index of the path given. Returns -1 if not found. */ 45 | ssize_t imv_navigator_find_path(struct imv_navigator *nav, const char *path); 46 | 47 | /* Returns 1 if either the currently selected path or underlying file has 48 | * changed since last called */ 49 | int imv_navigator_poll_changed(struct imv_navigator *nav); 50 | 51 | /* Check whether navigator wrapped around paths list */ 52 | int imv_navigator_wrapped(struct imv_navigator *nav); 53 | 54 | /* Return how many paths in navigator */ 55 | size_t imv_navigator_length(struct imv_navigator *nav); 56 | 57 | /* Return a path for a given index */ 58 | char *imv_navigator_at(struct imv_navigator *nav, size_t index); 59 | 60 | 61 | #endif 62 | 63 | 64 | /* vim:set ts=2 sts=2 sw=2 et: */ 65 | -------------------------------------------------------------------------------- /src/canvas.h: -------------------------------------------------------------------------------- 1 | #ifndef IMV_CANVAS_H 2 | #define IMV_CANVAS_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | struct imv_canvas; 9 | struct imv_image; 10 | 11 | enum upscaling_method { 12 | UPSCALING_LINEAR, 13 | UPSCALING_NEAREST_NEIGHBOUR, 14 | UPSCALING_METHOD_COUNT, 15 | }; 16 | 17 | /* Create a canvas instance */ 18 | struct imv_canvas *imv_canvas_create(int width, int height); 19 | 20 | /* Clean up a canvas */ 21 | void imv_canvas_free(struct imv_canvas *canvas); 22 | 23 | /* Set the buffer size of the canvas */ 24 | void imv_canvas_resize(struct imv_canvas *canvas, int width, int height, double scale); 25 | 26 | /* Blank the canvas to be empty and transparent */ 27 | void imv_canvas_clear(struct imv_canvas *canvas); 28 | 29 | /* Set the current drawing color of the canvas */ 30 | void imv_canvas_color(struct imv_canvas *canvas, float r, float g, float b, float a); 31 | 32 | /* Fill a rectangle on the canvas with the current color */ 33 | void imv_canvas_fill_rectangle(struct imv_canvas *canvas, int x, int y, int width, int height); 34 | 35 | /* Fill the whole canvas with the current color */ 36 | void imv_canvas_fill(struct imv_canvas *canvas); 37 | 38 | /* Blit the given image area with a chequerboard pattern to the current OpenGL framebuffer */ 39 | void imv_canvas_fill_checkers(struct imv_canvas *canvas, struct imv_image *image, 40 | int x, int y, double scale, 41 | double rotation, bool mirrored); 42 | 43 | /* Select the font to draw text with */ 44 | void imv_canvas_font(struct imv_canvas *canvas, const char *name, int size); 45 | 46 | /* Prepare layout containing the given string, ready for rendering on the given 47 | * canvas. The caller is responsible for releasing it with a call to 48 | * g_object_unref */ 49 | PangoLayout *imv_canvas_make_layout(struct imv_canvas *canvas, const char *str); 50 | 51 | /* Shows layout with at the specified coordinates */ 52 | void imv_canvas_show_layout(struct imv_canvas *canvas, int x, int y, 53 | PangoLayout *layout); 54 | 55 | /* Draw some text on the canvas, returns the width used in pixels */ 56 | int imv_canvas_printf(struct imv_canvas *canvas, int x, int y, const char *fmt, ...); 57 | 58 | /* Blit the canvas to the current OpenGL framebuffer */ 59 | void imv_canvas_draw(struct imv_canvas *canvas); 60 | 61 | /* Blit the given image to the current OpenGL framebuffer */ 62 | void imv_canvas_draw_image(struct imv_canvas *canvas, struct imv_image *image, 63 | int x, int y, double scale, 64 | double rotation, bool mirrored, 65 | enum upscaling_method upscaling_method, 66 | bool cache_invalidated); 67 | 68 | #endif 69 | -------------------------------------------------------------------------------- /src/ipc.c: -------------------------------------------------------------------------------- 1 | #include "ipc.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | struct imv_ipc { 12 | int fd; 13 | imv_ipc_callback callback; 14 | void *data; 15 | }; 16 | 17 | struct connection { 18 | struct imv_ipc *ipc; 19 | int fd; 20 | }; 21 | 22 | static void *wait_for_commands(void* void_conn) 23 | { 24 | struct connection *conn = void_conn; 25 | 26 | while (1) { 27 | char buf[1024]; 28 | ssize_t len = recv(conn->fd, buf, sizeof buf - 1, 0); 29 | if (len <= 0) { 30 | break; 31 | } 32 | 33 | buf[len] = 0; 34 | while (len > 0 && isspace(buf[len-1])) { 35 | buf[len-1] = 0; 36 | --len; 37 | } 38 | 39 | if (conn->ipc->callback) { 40 | conn->ipc->callback(buf, conn->ipc->data); 41 | } 42 | } 43 | 44 | close(conn->fd); 45 | free(conn); 46 | return NULL; 47 | } 48 | 49 | static void *wait_for_connections(void* void_ipc) 50 | { 51 | struct imv_ipc *ipc = void_ipc; 52 | (void)ipc; 53 | 54 | while (1) { 55 | int client = accept(ipc->fd, NULL, NULL); 56 | if (client == -1) { 57 | break; 58 | } 59 | struct connection *conn = calloc(1, sizeof *conn); 60 | conn->ipc = ipc; 61 | conn->fd = client; 62 | 63 | pthread_t thread; 64 | pthread_create(&thread, NULL, wait_for_commands, conn); 65 | pthread_detach(thread); 66 | } 67 | return NULL; 68 | } 69 | 70 | struct imv_ipc *imv_ipc_create(void) 71 | { 72 | int sockfd = socket(AF_UNIX, SOCK_STREAM, 0); 73 | if (sockfd < 0) { 74 | return NULL; 75 | } 76 | 77 | struct sockaddr_un desc = { 78 | .sun_family = AF_UNIX 79 | }; 80 | imv_ipc_path(desc.sun_path, sizeof desc.sun_path, getpid()); 81 | 82 | unlink(desc.sun_path); 83 | 84 | if (bind(sockfd, (struct sockaddr*)&desc, sizeof desc) == -1) { 85 | close(sockfd); 86 | return NULL; 87 | } 88 | 89 | if (listen(sockfd, 5) == -1) { 90 | close(sockfd); 91 | return NULL; 92 | } 93 | 94 | struct imv_ipc *ipc = calloc(1, sizeof *ipc); 95 | if (ipc == NULL) { 96 | return NULL; 97 | } 98 | ipc->fd = sockfd; 99 | 100 | pthread_t thread; 101 | pthread_create(&thread, NULL, wait_for_connections, ipc); 102 | pthread_detach(thread); 103 | return ipc; 104 | } 105 | 106 | void imv_ipc_free(struct imv_ipc *ipc) 107 | { 108 | if (!ipc) { 109 | return; 110 | } 111 | 112 | char ipc_filename[1024]; 113 | imv_ipc_path(ipc_filename, sizeof ipc_filename, getpid()); 114 | unlink(ipc_filename); 115 | close(ipc->fd); 116 | 117 | free(ipc); 118 | } 119 | 120 | void imv_ipc_set_command_callback(struct imv_ipc *ipc, 121 | imv_ipc_callback callback, void *data) 122 | { 123 | ipc->callback = callback; 124 | ipc->data = data; 125 | } 126 | 127 | -------------------------------------------------------------------------------- /src/backend_librsvg.c: -------------------------------------------------------------------------------- 1 | #include "backend.h" 2 | #include "image.h" 3 | #include "source.h" 4 | #include "source_private.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | /* Some systems like GNU/Hurd don't define PATH_MAX */ 11 | #ifndef PATH_MAX 12 | #define PATH_MAX 4096 13 | #endif 14 | 15 | struct private { 16 | void *data; 17 | size_t len; 18 | char path[PATH_MAX+8]; 19 | }; 20 | 21 | static void free_private(void *raw_private) 22 | { 23 | free(raw_private); 24 | } 25 | 26 | static void load_image(void *raw_private, struct imv_image **image, int *frametime) 27 | { 28 | *image = NULL; 29 | *frametime = 0; 30 | 31 | struct private *private = raw_private; 32 | 33 | RsvgHandle *handle = NULL; 34 | GError *error = NULL; 35 | 36 | if (private->data) { 37 | handle = rsvg_handle_new_from_data(private->data, private->len, &error); 38 | } else { 39 | handle = rsvg_handle_new_from_file(private->path, &error); 40 | } 41 | 42 | if (handle) { 43 | *image = imv_image_create_from_svg(handle); 44 | } 45 | } 46 | 47 | static const struct imv_source_vtable vtable = { 48 | .load_first_frame = load_image, 49 | .free = free_private 50 | }; 51 | 52 | static enum backend_result open_path(const char *path, struct imv_source **src) 53 | { 54 | /* Look for an tag near the start of the file */ 55 | char header[4096]; 56 | FILE *f = fopen(path, "rb"); 57 | if (!f) { 58 | return BACKEND_BAD_PATH; 59 | } 60 | fread(header, 1, sizeof header, f); 61 | fclose(f); 62 | 63 | header[(sizeof header) - 1] = 0; 64 | if (!strstr(header, "data = NULL; 70 | private->len = 0; 71 | snprintf(private->path, sizeof private->path, "file://%s", path); 72 | 73 | *src = imv_source_create(&vtable, private); 74 | return BACKEND_SUCCESS; 75 | } 76 | 77 | static enum backend_result open_memory(void *data, size_t len, struct imv_source **src) 78 | { 79 | /* Look for an tag near the start of the file */ 80 | char header[4096]; 81 | size_t header_len = sizeof header; 82 | if (header_len > len) { 83 | header_len = len; 84 | } 85 | memcpy(header, data, header_len); 86 | header[header_len - 1] = 0; 87 | if (!strstr(header, "data = data; 93 | private->len = len; 94 | 95 | *src = imv_source_create(&vtable, private); 96 | return BACKEND_SUCCESS; 97 | } 98 | 99 | const struct imv_backend imv_backend_librsvg = { 100 | .name = "libRSVG", 101 | .description = "SVG library developed by GNOME", 102 | .website = "https://wiki.gnome.org/Projects/LibRsvg", 103 | .license = "GNU Lesser General Public License v2.1+", 104 | .open_path = &open_path, 105 | .open_memory = &open_memory, 106 | }; 107 | 108 | -------------------------------------------------------------------------------- /src/window.h: -------------------------------------------------------------------------------- 1 | #ifndef IMV_WINDOW_H 2 | #define IMV_WINDOW_H 3 | 4 | #include 5 | 6 | struct imv_window; 7 | 8 | enum imv_event_type { 9 | IMV_EVENT_CLOSE, 10 | IMV_EVENT_RESIZE, 11 | IMV_EVENT_KEYBOARD, 12 | IMV_EVENT_KEYBOARD_MODS, 13 | IMV_EVENT_MOUSE_MOTION, 14 | IMV_EVENT_MOUSE_BUTTON, 15 | IMV_EVENT_MOUSE_SCROLL, 16 | IMV_EVENT_CUSTOM 17 | }; 18 | 19 | struct imv_event { 20 | enum imv_event_type type; 21 | union { 22 | struct { 23 | int width; 24 | int height; 25 | int buffer_width; 26 | int buffer_height; 27 | double scale; 28 | } resize; 29 | struct { 30 | int scancode; 31 | char *keyname; 32 | char *description; 33 | char *text; 34 | } keyboard; 35 | struct { 36 | double x, y, dx, dy; 37 | } mouse_motion; 38 | struct { 39 | int button; 40 | bool pressed; 41 | } mouse_button; 42 | struct { 43 | double dx, dy; 44 | } mouse_scroll; 45 | void *custom; 46 | } data; 47 | }; 48 | 49 | /* Create a new window */ 50 | struct imv_window *imv_window_create(int w, int h, const char *title); 51 | 52 | /* Clean up an imv_window instance */ 53 | void imv_window_free(struct imv_window *window); 54 | 55 | /* Clears the window with the given colour */ 56 | void imv_window_clear(struct imv_window *window, unsigned char r, 57 | unsigned char g, unsigned char b); 58 | 59 | /* Get the logical/event/window manager size of the window */ 60 | void imv_window_get_size(struct imv_window *window, int *w, int *h); 61 | 62 | /* Get the pixel dimensions that the window is rendered at */ 63 | void imv_window_get_framebuffer_size(struct imv_window *window, int *w, int *h); 64 | 65 | /* Set the window's title */ 66 | void imv_window_set_title(struct imv_window *window, const char *title); 67 | 68 | /* Returns true if the window is fullscreen */ 69 | bool imv_window_is_fullscreen(struct imv_window *window); 70 | 71 | /* Set the window's fullscreen state */ 72 | void imv_window_set_fullscreen(struct imv_window *window, bool fullscreen); 73 | 74 | /* Check whether a given mouse button is pressed */ 75 | bool imv_window_get_mouse_button(struct imv_window *window, int button); 76 | 77 | /* Gets the mouse's position */ 78 | void imv_window_get_mouse_position(struct imv_window *window, double *x, double *y); 79 | 80 | /* Swap the framebuffers. Present anything rendered since the last call. */ 81 | void imv_window_present(struct imv_window *window); 82 | 83 | /* Blocks until an event is received, or the timeout (in seconds) expires */ 84 | void imv_window_wait_for_event(struct imv_window *window, double timeout); 85 | 86 | /* Push an event to the event queue. An internal copy of the event is made. 87 | * Wakes up imv_window_wait_for_event */ 88 | void imv_window_push_event(struct imv_window *window, struct imv_event *e); 89 | 90 | typedef void (*imv_event_handler)(void *data, const struct imv_event *e); 91 | 92 | /* When called, the handler function is called for each event on the event 93 | * queue */ 94 | void imv_window_pump_events(struct imv_window *window, imv_event_handler handler, void *data); 95 | 96 | #endif 97 | -------------------------------------------------------------------------------- /src/commands.c: -------------------------------------------------------------------------------- 1 | #include "commands.h" 2 | #include "list.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | struct imv_commands { 9 | struct list *command_list; 10 | }; 11 | 12 | struct command { 13 | char* command; 14 | void (*handler)(struct list *args, const char *argstr, void *data); 15 | char* alias; 16 | }; 17 | 18 | struct imv_commands *imv_commands_create(void) 19 | { 20 | struct imv_commands *cmds = malloc(sizeof *cmds); 21 | cmds->command_list = list_create(); 22 | return cmds; 23 | } 24 | 25 | void imv_commands_free(struct imv_commands *cmds) 26 | { 27 | for(size_t i = 0; i < cmds->command_list->len; ++i) { 28 | struct command *cmd = cmds->command_list->items[i]; 29 | free(cmd->command); 30 | if(cmd->alias) { 31 | free(cmd->alias); 32 | } 33 | free(cmd); 34 | } 35 | list_free(cmds->command_list); 36 | free(cmds); 37 | } 38 | 39 | void imv_command_register(struct imv_commands *cmds, const char *command, void (*handler)(struct list*, const char*, void*)) 40 | { 41 | struct command *cmd = malloc(sizeof *cmd); 42 | cmd->command = strdup(command); 43 | cmd->handler = handler; 44 | cmd->alias = NULL; 45 | list_append(cmds->command_list, cmd); 46 | } 47 | 48 | void imv_command_alias(struct imv_commands *cmds, const char *command, const char *alias) 49 | { 50 | struct command *cmd = malloc(sizeof *cmd); 51 | cmd->command = strdup(command); 52 | cmd->handler = NULL; 53 | cmd->alias = strdup(alias); 54 | list_append(cmds->command_list, cmd); 55 | } 56 | 57 | int imv_command_exec(struct imv_commands *cmds, const char *command, void *data) 58 | { 59 | struct list *args = list_from_string(command, ' '); 60 | int ret = 1; 61 | 62 | if(args->len > 0) { 63 | for(size_t i = 0; i < cmds->command_list->len; ++i) { 64 | struct command *cmd = cmds->command_list->items[i]; 65 | if(!strcmp(cmd->command, args->items[0])) { 66 | if(cmd->handler) { 67 | /* argstr = all args as a single string */ 68 | const char *argstr = command + strlen(cmd->command) + 1; 69 | cmd->handler(args, argstr, data); 70 | ret = 0; 71 | } else if(cmd->alias) { 72 | char *new_args = list_to_string(args, " ", 1); 73 | size_t cmd_len = strlen(cmd->alias) + 1 + strlen(new_args) + 1; 74 | char *new_cmd = malloc(cmd_len); 75 | snprintf(new_cmd, cmd_len, "%s %s", cmd->alias, new_args); 76 | ret = imv_command_exec(cmds, new_cmd, data); 77 | free(new_args); 78 | free(new_cmd); 79 | } 80 | break; 81 | } 82 | } 83 | } 84 | 85 | list_deep_free(args); 86 | return ret; 87 | } 88 | 89 | int imv_command_exec_list(struct imv_commands *cmds, struct list *commands, void *data) 90 | { 91 | int ret = 0; 92 | for(size_t i = 0; i < commands->len; ++i) { 93 | const char *command = commands->items[i]; 94 | ret += imv_command_exec(cmds, command, data); 95 | } 96 | return ret; 97 | } 98 | 99 | /* vim:set ts=2 sts=2 sw=2 et: */ 100 | -------------------------------------------------------------------------------- /src/list.c: -------------------------------------------------------------------------------- 1 | #include "list.h" 2 | 3 | #include 4 | 5 | struct list *list_create(void) 6 | { 7 | struct list *list = malloc(sizeof *list); 8 | list->len = 0; 9 | list->cap = 64; 10 | list->items = malloc(sizeof(void*) * list->cap); 11 | return list; 12 | } 13 | 14 | void list_free(struct list *list) 15 | { 16 | if (list) { 17 | free(list->items); 18 | free(list); 19 | } 20 | } 21 | 22 | void list_deep_free(struct list *list) 23 | { 24 | for(size_t i = 0; i < list->len; ++i) { 25 | free(list->items[i]); 26 | } 27 | list_free(list); 28 | } 29 | 30 | void list_append(struct list *list, void *item) 31 | { 32 | list_grow(list, list->len + 1); 33 | list->items[list->len++] = item; 34 | } 35 | 36 | void list_grow(struct list *list, size_t min_size) 37 | { 38 | if(list->cap >= min_size) { 39 | return; 40 | } 41 | 42 | while(list->cap < min_size) { 43 | list->cap *= 2; 44 | } 45 | 46 | list->items = realloc(list->items, sizeof(void*) * list->cap); 47 | } 48 | 49 | void list_remove(struct list *list, size_t index) 50 | { 51 | if(index >= list->len) { 52 | return; 53 | } 54 | 55 | memmove(&list->items[index], &list->items[index + 1], sizeof(void*) * (list->len - index)); 56 | 57 | list->len -= 1; 58 | } 59 | 60 | void list_insert(struct list *list, size_t index, void *item) 61 | { 62 | list_grow(list, list->len + 1); 63 | 64 | if(index > list->len) { 65 | index = list->len; 66 | } 67 | 68 | memmove(&list->items[index + 1], &list->items[index], sizeof(void*) * (list->len - index)); 69 | list->items[index] = item; 70 | list->len += 1; 71 | } 72 | 73 | void list_clear(struct list *list) 74 | { 75 | list->len = 0; 76 | } 77 | 78 | struct list *list_from_string(const char *string, char delim) 79 | { 80 | struct list *list = list_create(); 81 | 82 | const char *base = string; 83 | 84 | while(*base) { 85 | while(*base && *base == delim) { 86 | ++base; 87 | } 88 | 89 | const char *end = base; 90 | while(*end && *end != delim) { 91 | ++end; 92 | } 93 | 94 | if(*base && base != end) { 95 | char *item = strndup(base, end - base); 96 | list_append(list, item); 97 | base = end; 98 | } 99 | } 100 | 101 | return list; 102 | } 103 | 104 | int list_find(struct list *list, int (*cmp)(const void *, const void *), const void *key) 105 | { 106 | for(size_t i = 0; i < list->len; ++i) { 107 | if(!cmp(list->items[i], key)) { 108 | return (int)i; 109 | } 110 | } 111 | return -1; 112 | } 113 | 114 | char *list_to_string(struct list *list, const char *sep, size_t start) 115 | { 116 | size_t len = 0; 117 | size_t cap = 512; 118 | char *buf = malloc(cap); 119 | buf[0] = 0; 120 | 121 | size_t sep_len = strlen(sep); 122 | for (size_t i = start; i < list->len; ++i) { 123 | size_t item_len = strlen(list->items[i]); 124 | if (len + item_len + sep_len >= cap) { 125 | cap *= 2; 126 | buf = realloc(buf, cap); 127 | assert(buf); 128 | } 129 | 130 | strncat(buf, list->items[i], cap - 1); 131 | len += item_len; 132 | 133 | strncat(buf, sep, cap - 1); 134 | len += sep_len; 135 | } 136 | return buf; 137 | } 138 | 139 | /* vim:set ts=2 sts=2 sw=2 et: */ 140 | -------------------------------------------------------------------------------- /src/source.c: -------------------------------------------------------------------------------- 1 | #include "source.h" 2 | #include "source_private.h" 3 | 4 | #include 5 | #include 6 | 7 | struct imv_source { 8 | /* pointers to implementation's functions */ 9 | const struct imv_source_vtable *vtable; 10 | 11 | /* pointer to implementation data */ 12 | void *private; 13 | 14 | /* Attempted to be locked by load_first_frame or load_next_frame. 15 | * If the mutex can't be locked, the call is aborted. 16 | * Used to prevent the source from having multiple worker threads at once. 17 | * Released by the source before calling the message callback with a result. 18 | */ 19 | pthread_mutex_t busy; 20 | 21 | /* callback function */ 22 | imv_source_callback callback; 23 | /* callback data */ 24 | void *callback_data; 25 | }; 26 | 27 | struct imv_source *imv_source_create(const struct imv_source_vtable *vtable, void *private) 28 | { 29 | struct imv_source *source = calloc(1, sizeof *source); 30 | source->vtable = vtable; 31 | source->private = private; 32 | pthread_mutex_init(&source->busy, NULL); 33 | return source; 34 | } 35 | 36 | static void *free_thread(void *src) 37 | { 38 | imv_source_free(src); 39 | return NULL; 40 | } 41 | 42 | void imv_source_async_free(struct imv_source *src) 43 | { 44 | pthread_t thread; 45 | pthread_create(&thread, NULL, free_thread, src); 46 | pthread_detach(thread); 47 | } 48 | 49 | static void *first_frame_thread(void *src) 50 | { 51 | imv_source_load_first_frame(src); 52 | return NULL; 53 | } 54 | 55 | void imv_source_async_load_first_frame(struct imv_source *src) 56 | { 57 | pthread_t thread; 58 | pthread_create(&thread, NULL, first_frame_thread, src); 59 | pthread_detach(thread); 60 | } 61 | 62 | static void *next_frame_thread(void *src) 63 | { 64 | imv_source_load_next_frame(src); 65 | return NULL; 66 | } 67 | void imv_source_async_load_next_frame(struct imv_source *src) 68 | { 69 | pthread_t thread; 70 | pthread_create(&thread, NULL, next_frame_thread, src); 71 | pthread_detach(thread); 72 | } 73 | 74 | void imv_source_free(struct imv_source *src) 75 | { 76 | pthread_mutex_lock(&src->busy); 77 | src->vtable->free(src->private); 78 | pthread_mutex_unlock(&src->busy); 79 | pthread_mutex_destroy(&src->busy); 80 | free(src); 81 | } 82 | 83 | void imv_source_load_first_frame(struct imv_source *src) 84 | { 85 | if (!src->vtable->load_first_frame) { 86 | return; 87 | } 88 | 89 | if (pthread_mutex_trylock(&src->busy)) { 90 | return; 91 | } 92 | 93 | struct imv_source_message msg = { 94 | .source = src, 95 | .user_data = src->callback_data 96 | }; 97 | 98 | src->vtable->load_first_frame(src->private, &msg.image, &msg.frametime); 99 | 100 | pthread_mutex_unlock(&src->busy); 101 | 102 | src->callback(&msg); 103 | } 104 | 105 | void imv_source_load_next_frame(struct imv_source *src) 106 | { 107 | if (!src->vtable->load_next_frame) { 108 | return; 109 | } 110 | 111 | if (pthread_mutex_trylock(&src->busy)) { 112 | return; 113 | } 114 | 115 | struct imv_source_message msg = { 116 | .source = src, 117 | .user_data = src->callback_data 118 | }; 119 | 120 | src->vtable->load_next_frame(src->private, &msg.image, &msg.frametime); 121 | 122 | pthread_mutex_unlock(&src->busy); 123 | 124 | src->callback(&msg); 125 | } 126 | 127 | void imv_source_set_callback(struct imv_source *src, imv_source_callback callback, 128 | void *data) 129 | { 130 | src->callback = callback; 131 | src->callback_data = data; 132 | } 133 | -------------------------------------------------------------------------------- /src/backend_libheif.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "backend.h" 6 | #include "bitmap.h" 7 | #include "image.h" 8 | #include "source_private.h" 9 | 10 | struct private { 11 | struct heif_image *img; 12 | }; 13 | 14 | static void free_private(void *raw_private) 15 | { 16 | if (!raw_private) { 17 | return; 18 | } 19 | struct private *private = raw_private; 20 | heif_image_release(private->img); 21 | free(private); 22 | } 23 | 24 | static void load_image(void *raw_private, struct imv_image **image, int *frametime) 25 | { 26 | *image = NULL; 27 | *frametime = 0; 28 | 29 | struct private *private = raw_private; 30 | 31 | int stride; 32 | const uint8_t *data = heif_image_get_plane_readonly(private->img, heif_channel_interleaved, &stride); 33 | 34 | int width = heif_image_get_width(private->img, heif_channel_interleaved); 35 | int height = heif_image_get_height(private->img, heif_channel_interleaved); 36 | unsigned char *bitmap = malloc(width * height * 4); 37 | memcpy(bitmap, data, width * height * 4); 38 | 39 | struct imv_bitmap *bmp = malloc(sizeof *bmp); 40 | bmp->width = width, 41 | bmp->height = height, 42 | bmp->format = IMV_ABGR; 43 | bmp->data = bitmap; 44 | *image = imv_image_create_from_bitmap(bmp); 45 | } 46 | 47 | static const struct imv_source_vtable vtable = { 48 | .load_first_frame = load_image, 49 | .free = free_private, 50 | }; 51 | 52 | struct heif_error get_primary_image(struct heif_context *ctx, struct heif_image **img) 53 | { 54 | struct heif_image_handle *handle; 55 | struct heif_error err = heif_context_get_primary_image_handle(ctx, &handle); 56 | if (err.code != heif_error_Ok) { 57 | return err; 58 | } 59 | 60 | err = heif_decode_image(handle, img, heif_colorspace_RGB, heif_chroma_interleaved_RGBA, NULL); 61 | heif_image_handle_release(handle); 62 | return err; 63 | } 64 | 65 | static enum backend_result open_path(const char *path, struct imv_source **src) 66 | { 67 | struct heif_context *ctx = heif_context_alloc(); 68 | struct heif_error err = heif_context_read_from_file(ctx, path, NULL); // TODO: error 69 | if (err.code != heif_error_Ok) { 70 | heif_context_free(ctx); 71 | if (err.code == heif_error_Input_does_not_exist) { 72 | return BACKEND_BAD_PATH; 73 | } 74 | return BACKEND_UNSUPPORTED; 75 | } 76 | 77 | struct heif_image *img; 78 | err = get_primary_image(ctx, &img); 79 | heif_context_free(ctx); 80 | if (err.code != heif_error_Ok) { 81 | return BACKEND_UNSUPPORTED; 82 | } 83 | 84 | struct private *private = malloc(sizeof *private); 85 | private->img = img; 86 | *src = imv_source_create(&vtable, private); 87 | return BACKEND_SUCCESS; 88 | } 89 | 90 | static enum backend_result open_memory(void *data, size_t len, struct imv_source **src) 91 | { 92 | struct heif_context *ctx = heif_context_alloc(); 93 | struct heif_error err = heif_context_read_from_memory_without_copy(ctx, data, len, NULL); 94 | if (err.code != heif_error_Ok) { 95 | heif_context_free(ctx); 96 | return BACKEND_UNSUPPORTED; 97 | } 98 | 99 | struct heif_image *img; 100 | err = get_primary_image(ctx, &img); 101 | heif_context_free(ctx); 102 | if (err.code != heif_error_Ok) { 103 | return BACKEND_UNSUPPORTED; 104 | } 105 | 106 | struct private *private = malloc(sizeof *private); 107 | private->img = img; 108 | *src = imv_source_create(&vtable, private); 109 | return BACKEND_SUCCESS; 110 | } 111 | 112 | const struct imv_backend imv_backend_libheif = { 113 | .name = "libheif", 114 | .description = "ISO/IEC 23008-12:2017 HEIF file format decoder and encoder.", 115 | .website = "http://www.libheif.org", 116 | .license = "GNU Lesser General Public License", 117 | .open_path = &open_path, 118 | .open_memory = &open_memory, 119 | }; 120 | -------------------------------------------------------------------------------- /src/backend_libjpeg.c: -------------------------------------------------------------------------------- 1 | #include "backend.h" 2 | #include "bitmap.h" 3 | #include "image.h" 4 | #include "source.h" 5 | #include "source_private.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | 17 | struct private { 18 | int fd; 19 | void *data; 20 | size_t len; 21 | tjhandle jpeg; 22 | int width; 23 | int height; 24 | }; 25 | 26 | static void free_private(void *raw_private) 27 | { 28 | if (!raw_private) { 29 | return; 30 | } 31 | struct private *private = raw_private; 32 | tjDestroy(private->jpeg); 33 | if (private->fd >= 0) { 34 | munmap(private->data, private->len); 35 | close(private->fd); 36 | } 37 | 38 | free(private); 39 | } 40 | 41 | static void load_image(void *raw_private, struct imv_image **image, int *frametime) 42 | { 43 | *image = NULL; 44 | *frametime = 0; 45 | 46 | struct private *private = raw_private; 47 | 48 | void *bitmap = malloc(private->height * private->width * 4); 49 | int rcode = tjDecompress2(private->jpeg, private->data, private->len, 50 | bitmap, private->width, 0, private->height, TJPF_RGBA, TJFLAG_FASTDCT); 51 | 52 | if (rcode) { 53 | free(bitmap); 54 | return; 55 | } 56 | 57 | struct imv_bitmap *bmp = malloc(sizeof *bmp); 58 | bmp->width = private->width; 59 | bmp->height = private->height; 60 | bmp->format = IMV_ABGR; 61 | bmp->data = bitmap; 62 | *image = imv_image_create_from_bitmap(bmp); 63 | } 64 | 65 | static const struct imv_source_vtable vtable = { 66 | .load_first_frame = load_image, 67 | .free = free_private 68 | }; 69 | 70 | static enum backend_result open_path(const char *path, struct imv_source **src) 71 | { 72 | struct private private; 73 | 74 | private.fd = open(path, O_RDONLY); 75 | if (private.fd < 0) { 76 | return BACKEND_BAD_PATH; 77 | } 78 | 79 | off_t len = lseek(private.fd, 0, SEEK_END); 80 | if (len < 0) { 81 | close(private.fd); 82 | return BACKEND_BAD_PATH; 83 | } 84 | 85 | private.len = len; 86 | 87 | private.data = mmap(NULL, private.len, PROT_READ, MAP_PRIVATE, private.fd, 0); 88 | if (private.data == MAP_FAILED || !private.data) { 89 | close(private.fd); 90 | return BACKEND_BAD_PATH; 91 | } 92 | 93 | private.jpeg = tjInitDecompress(); 94 | if (!private.jpeg) { 95 | munmap(private.data, private.len); 96 | close(private.fd); 97 | return BACKEND_UNSUPPORTED; 98 | } 99 | 100 | int rcode = tjDecompressHeader(private.jpeg, private.data, private.len, 101 | &private.width, &private.height); 102 | if (rcode) { 103 | tjDestroy(private.jpeg); 104 | munmap(private.data, private.len); 105 | close(private.fd); 106 | return BACKEND_UNSUPPORTED; 107 | } 108 | 109 | struct private *new_private = malloc(sizeof private); 110 | memcpy(new_private, &private, sizeof private); 111 | 112 | *src = imv_source_create(&vtable, new_private); 113 | return BACKEND_SUCCESS; 114 | } 115 | 116 | static enum backend_result open_memory(void *data, size_t len, struct imv_source **src) 117 | { 118 | struct private private; 119 | 120 | private.fd = -1; 121 | private.data = data; 122 | private.len = len; 123 | 124 | private.jpeg = tjInitDecompress(); 125 | if (!private.jpeg) { 126 | return BACKEND_UNSUPPORTED; 127 | } 128 | 129 | int rcode = tjDecompressHeader(private.jpeg, private.data, private.len, 130 | &private.width, &private.height); 131 | if (rcode) { 132 | tjDestroy(private.jpeg); 133 | return BACKEND_UNSUPPORTED; 134 | } 135 | 136 | struct private *new_private = malloc(sizeof private); 137 | memcpy(new_private, &private, sizeof private); 138 | 139 | *src = imv_source_create(&vtable, new_private); 140 | return BACKEND_SUCCESS; 141 | } 142 | 143 | const struct imv_backend imv_backend_libjpeg = { 144 | .name = "libjpeg-turbo", 145 | .description = "Fast JPEG codec based on libjpeg. " 146 | "This software is based in part on the work " 147 | "of the Independent JPEG Group.", 148 | .website = "https://libjpeg-turbo.org/", 149 | .license = "The Modified BSD License", 150 | .open_path = &open_path, 151 | .open_memory = &open_memory, 152 | }; 153 | -------------------------------------------------------------------------------- /test/navigator.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "navigator.h" 11 | 12 | #define FILENAME1 "example.file.1" 13 | #define FILENAME2 "example.file.2" 14 | #define FILENAME3 "example.file.3" 15 | #define FILENAME4 "example.file.4" 16 | #define FILENAME5 "example.file.5" 17 | #define FILENAME6 "example.file.6" 18 | 19 | static void test_navigator_add_remove(void **state) 20 | { 21 | (void)state; 22 | struct imv_navigator *nav = imv_navigator_create(); 23 | 24 | /* Check poll_changed */ 25 | assert_false(imv_navigator_poll_changed(nav)); 26 | 27 | /* Add 6 paths, one non-existent should fail */ 28 | assert_false(imv_navigator_add(nav, FILENAME1, 0)); 29 | assert_false(imv_navigator_add(nav, FILENAME2, 0)); 30 | assert_false(imv_navigator_add(nav, FILENAME3, 0)); 31 | assert_false(imv_navigator_add(nav, FILENAME4, 0)); 32 | assert_false(imv_navigator_add(nav, FILENAME5, 0)); 33 | assert_false(imv_navigator_add(nav, FILENAME6, 0)); 34 | assert_int_equal(imv_navigator_length(nav), 6); 35 | 36 | /* Check poll_changed */ 37 | assert_true(imv_navigator_poll_changed(nav)); 38 | 39 | /* Make sure current selection is #1 */ 40 | assert_string_equal(imv_navigator_selection(nav), FILENAME1); 41 | 42 | /* Move right and remove current file (#2); should get to #3 */ 43 | imv_navigator_select_rel(nav, 1); 44 | assert_string_equal(imv_navigator_selection(nav), FILENAME2); 45 | imv_navigator_remove(nav, FILENAME2); 46 | assert_int_equal(imv_navigator_length(nav), 5); 47 | assert_string_equal(imv_navigator_selection(nav), FILENAME3); 48 | 49 | /* Move left and remove current file (#1); should get to #6 */ 50 | imv_navigator_select_rel(nav, -1); 51 | assert_string_equal(imv_navigator_selection(nav), FILENAME1); 52 | imv_navigator_remove(nav, FILENAME1); 53 | assert_string_equal(imv_navigator_selection(nav), FILENAME6); 54 | 55 | /* Move left, right, remove current file (#6); should get to #3 */ 56 | imv_navigator_select_rel(nav, -1); 57 | imv_navigator_select_rel(nav, 1); 58 | assert_string_equal(imv_navigator_selection(nav), FILENAME6); 59 | imv_navigator_remove(nav, FILENAME6); 60 | assert_string_equal(imv_navigator_selection(nav), FILENAME3); 61 | 62 | /* Remove #4; should not move */ 63 | imv_navigator_remove(nav, FILENAME4); 64 | assert_string_equal(imv_navigator_selection(nav), FILENAME3); 65 | 66 | /* Verify that #4 is removed by moving left; should get to #5 */ 67 | imv_navigator_select_rel(nav, 1); 68 | assert_string_equal(imv_navigator_selection(nav), FILENAME5); 69 | 70 | imv_navigator_free(nav); 71 | } 72 | 73 | static void test_navigator_file_changed(void **state) 74 | { 75 | int fd; 76 | struct imv_navigator *nav = imv_navigator_create(); 77 | struct timespec times[2] = { {0, 0}, {0, 0} }; 78 | 79 | (void)state; 80 | 81 | fd = open(FILENAME1, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR); 82 | if (fd == -1) { 83 | imv_navigator_free(nav); 84 | (void)unlink(FILENAME1); 85 | skip(); 86 | } 87 | assert_false(futimens(fd, times) == -1); 88 | 89 | assert_false(imv_navigator_add(nav, FILENAME1, 0)); 90 | assert_true(imv_navigator_poll_changed(nav)); 91 | assert_false(imv_navigator_poll_changed(nav)); 92 | 93 | assert_false(sleep(1)); 94 | 95 | fd = open(FILENAME1, O_RDWR); 96 | assert_false(fd == -1); 97 | 98 | times[0].tv_nsec = UTIME_NOW; 99 | times[0].tv_sec = UTIME_NOW; 100 | times[1].tv_nsec = UTIME_NOW; 101 | times[1].tv_sec = UTIME_NOW; 102 | assert_false(futimens(fd, times) == -1); 103 | 104 | /* sleep to ensure we don't hit the poll rate-limiting */ 105 | sleep(1); 106 | 107 | assert_true(imv_navigator_poll_changed(nav)); 108 | 109 | (void)close(fd); 110 | (void)unlink(FILENAME1); 111 | imv_navigator_free(nav); 112 | } 113 | 114 | int main(void) 115 | { 116 | (void)test_navigator_add_remove; /* skipped for now */ 117 | const struct CMUnitTest tests[] = { 118 | /* cmocka_unit_test(test_navigator_add_remove), */ 119 | cmocka_unit_test(test_navigator_file_changed), 120 | }; 121 | 122 | return cmocka_run_group_tests(tests, NULL, NULL); 123 | } 124 | 125 | 126 | /* vim:set ts=2 sts=2 sw=2 et: */ 127 | -------------------------------------------------------------------------------- /src/viewport.h: -------------------------------------------------------------------------------- 1 | #ifndef IMV_VIEWPORT_H 2 | #define IMV_VIEWPORT_H 3 | 4 | #include 5 | #include "image.h" 6 | 7 | struct imv_viewport; 8 | 9 | enum scaling_mode { 10 | SCALING_NONE, 11 | SCALING_DOWN, 12 | SCALING_FULL, 13 | SCALING_CROP, 14 | SCALING_MODE_COUNT 15 | }; 16 | 17 | /* Used to signify how a a user requested a zoom */ 18 | enum imv_zoom_source { 19 | IMV_ZOOM_MOUSE, 20 | IMV_ZOOM_KEYBOARD 21 | }; 22 | 23 | /* Creates an instance of imv_viewport */ 24 | struct imv_viewport *imv_viewport_create(int window_width, int window_height, 25 | int buffer_width, int buffer_height); 26 | 27 | /* Cleans up an imv_viewport instance */ 28 | void imv_viewport_free(struct imv_viewport *view); 29 | 30 | /* Set playback of animated gifs */ 31 | void imv_viewport_set_playing(struct imv_viewport *view, bool playing); 32 | 33 | /* Get playback status of animated gifs */ 34 | bool imv_viewport_is_playing(struct imv_viewport *view); 35 | 36 | /* Toggle playback of animated gifs */ 37 | void imv_viewport_toggle_playing(struct imv_viewport *view); 38 | 39 | /* Fetch viewport offset/position */ 40 | void imv_viewport_get_offset(struct imv_viewport *view, int *x, int *y); 41 | 42 | /* Fetch viewport scale */ 43 | void imv_viewport_get_scale(struct imv_viewport *view, double *scale); 44 | 45 | /* Fetch viewport rotation */ 46 | void imv_viewport_get_rotation(struct imv_viewport *view, double *rotation); 47 | 48 | /* Fetch viewport mirror status */ 49 | void imv_viewport_get_mirrored(struct imv_viewport *view, bool *mirrored); 50 | 51 | /* Set the default pan_factor factor for the x and y position */ 52 | void imv_viewport_set_default_pan_factor(struct imv_viewport *view, double pan_factor_x, double pan_factor_y); 53 | 54 | /* Pan the view by the given amounts without letting the image get too far 55 | * off-screen */ 56 | void imv_viewport_move(struct imv_viewport *view, int x, int y, 57 | const struct imv_image *image); 58 | 59 | /* Zoom the view by the given amount. imv_image* is used to get the image 60 | * dimensions */ 61 | void imv_viewport_zoom(struct imv_viewport *view, const struct imv_image *image, 62 | enum imv_zoom_source, int mouse_x, int mouse_y, int amount); 63 | 64 | /* Rotate the view by the given number of degrees */ 65 | void imv_viewport_rotate_by(struct imv_viewport *view, double degrees); 66 | 67 | /* Rotate the view to the given number of degrees */ 68 | void imv_viewport_rotate_to(struct imv_viewport *view, double degrees); 69 | 70 | /* Flip horizontally (across vertical axis) */ 71 | void imv_viewport_flip_h(struct imv_viewport *view); 72 | 73 | /* Flip vertically (across horizontal axis) */ 74 | void imv_viewport_flip_v(struct imv_viewport *view); 75 | 76 | /* Flip vertically (across horizontal axis) */ 77 | void imv_viewport_reset_transform(struct imv_viewport *view); 78 | 79 | /* Recenter the view to be in the middle of the image */ 80 | void imv_viewport_center(struct imv_viewport *view, 81 | const struct imv_image *image); 82 | 83 | /* Scale the view so that the image appears at its actual resolution */ 84 | void imv_viewport_scale_to_actual(struct imv_viewport *view, 85 | const struct imv_image *image); 86 | 87 | /* Scale the view so that the image fits in the window */ 88 | void imv_viewport_scale_to_window(struct imv_viewport *view, 89 | const struct imv_image *image); 90 | 91 | /* Scale the view so that the image fills the window */ 92 | void imv_viewport_crop_to_window(struct imv_viewport *view, 93 | const struct imv_image *image); 94 | 95 | /* Rescale the view with the chosen scaling method */ 96 | void imv_viewport_rescale(struct imv_viewport *view, const struct imv_image *image, 97 | enum scaling_mode); 98 | 99 | /* Tell the viewport that it needs to be redrawn */ 100 | void imv_viewport_set_redraw(struct imv_viewport *view); 101 | 102 | /* Tell the viewport the window or image has changed */ 103 | void imv_viewport_update(struct imv_viewport *view, 104 | int window_width, int window_height, 105 | int buffer_width, int buffer_height, 106 | struct imv_image *image, enum scaling_mode); 107 | 108 | /* Poll whether we need to redraw */ 109 | int imv_viewport_needs_redraw(struct imv_viewport *view); 110 | 111 | #endif 112 | 113 | /* vim:set ts=2 sts=2 sw=2 et: */ 114 | -------------------------------------------------------------------------------- /src/backend_libpng.c: -------------------------------------------------------------------------------- 1 | #include "backend.h" 2 | #include "bitmap.h" 3 | #include "image.h" 4 | #include "log.h" 5 | #include "source.h" 6 | #include "source_private.h" 7 | 8 | #include 9 | 10 | #include 11 | 12 | struct private { 13 | FILE *file; 14 | png_structp png; 15 | png_infop info; 16 | }; 17 | 18 | static void free_private(void *raw_private) 19 | { 20 | if (!raw_private) { 21 | return; 22 | } 23 | 24 | struct private *private = raw_private; 25 | png_destroy_read_struct(&private->png, &private->info, NULL); 26 | if (private->file) { 27 | fclose(private->file); 28 | } 29 | free(private); 30 | } 31 | 32 | static void load_image(void *raw_private, struct imv_image **image, int *frametime) 33 | { 34 | *image = NULL; 35 | *frametime = 0; 36 | 37 | struct private *private = raw_private; 38 | if (setjmp(png_jmpbuf(private->png))) { 39 | return; 40 | } 41 | 42 | const int width = png_get_image_width(private->png, private->info); 43 | const int height = png_get_image_height(private->png, private->info); 44 | 45 | png_bytep *rows = malloc(sizeof(png_bytep) * height); 46 | size_t row_len = png_get_rowbytes(private->png, private->info); 47 | rows[0] = malloc(height * row_len); 48 | for (int y = 1; y < height; ++y) { 49 | rows[y] = rows[0] + row_len * y; 50 | } 51 | 52 | if (setjmp(png_jmpbuf(private->png))) { 53 | return; 54 | } 55 | 56 | png_read_image(private->png, rows); 57 | void *raw_bmp = rows[0]; 58 | free(rows); 59 | fclose(private->file); 60 | private->file = NULL; 61 | 62 | 63 | struct imv_bitmap *bmp = malloc(sizeof *bmp); 64 | bmp->width = width; 65 | bmp->height = height; 66 | bmp->format = IMV_ABGR; 67 | bmp->data = raw_bmp; 68 | *image = imv_image_create_from_bitmap(bmp); 69 | } 70 | 71 | static const struct imv_source_vtable vtable = { 72 | .load_first_frame = load_image, 73 | .free = free_private 74 | }; 75 | 76 | static enum backend_result open_path(const char *path, struct imv_source **src) 77 | { 78 | 79 | unsigned char header[8]; 80 | FILE *f = fopen(path, "rb"); 81 | if (!f) { 82 | return BACKEND_BAD_PATH; 83 | } 84 | fread(header, 1, sizeof header, f); 85 | if (png_sig_cmp(header, 0, sizeof header)) { 86 | fclose(f); 87 | return BACKEND_UNSUPPORTED; 88 | } 89 | 90 | struct private *private = calloc(1, sizeof *private); 91 | private->file = f; 92 | private->png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); 93 | if (!private->png) { 94 | fclose(private->file); 95 | free(private); 96 | return BACKEND_UNSUPPORTED; 97 | } 98 | 99 | /* set max PNG chunk size to 50MB, instead of 8MB default */ 100 | png_set_chunk_malloc_max(private->png, 1024 * 1024 * 50); 101 | 102 | private->info = png_create_info_struct(private->png); 103 | if (!private->info) { 104 | png_destroy_read_struct(&private->png, NULL, NULL); 105 | fclose(private->file); 106 | free(private); 107 | return BACKEND_UNSUPPORTED; 108 | } 109 | 110 | if (setjmp(png_jmpbuf(private->png))) { 111 | png_destroy_read_struct(&private->png, &private->info, NULL); 112 | fclose(private->file); 113 | free(private); 114 | return BACKEND_UNSUPPORTED; 115 | } 116 | 117 | png_init_io(private->png, private->file); 118 | png_set_sig_bytes(private->png, sizeof header); 119 | png_read_info(private->png, private->info); 120 | 121 | /* Tell libpng to give us a consistent output format */ 122 | png_set_gray_to_rgb(private->png); 123 | png_set_filler(private->png, 0xff, PNG_FILLER_AFTER); 124 | png_set_strip_16(private->png); 125 | png_set_expand(private->png); 126 | png_set_packing(private->png); 127 | png_read_update_info(private->png, private->info); 128 | imv_log(IMV_DEBUG, "libpng: info width=%d height=%d bit_depth=%d color_type=%d\n", 129 | png_get_image_width(private->png, private->info), 130 | png_get_image_height(private->png, private->info), 131 | png_get_bit_depth(private->png, private->info), 132 | png_get_color_type(private->png, private->info)); 133 | 134 | if (setjmp(png_jmpbuf(private->png))) { 135 | png_destroy_read_struct(&private->png, &private->info, NULL); 136 | fclose(private->file); 137 | free(private); 138 | return BACKEND_UNSUPPORTED; 139 | } 140 | 141 | *src = imv_source_create(&vtable, private); 142 | return BACKEND_SUCCESS; 143 | } 144 | 145 | const struct imv_backend imv_backend_libpng = { 146 | .name = "libpng", 147 | .description = "The official PNG reference implementation", 148 | .website = "http://www.libpng.org/pub/png/libpng.html", 149 | .license = "The libpng license", 150 | .open_path = &open_path, 151 | }; 152 | 153 | -------------------------------------------------------------------------------- /src/keyboard.c: -------------------------------------------------------------------------------- 1 | #include "keyboard.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | struct imv_keyboard { 11 | struct xkb_context *context; 12 | struct xkb_keymap *keymap; 13 | struct xkb_state *state; 14 | }; 15 | 16 | struct imv_keyboard *imv_keyboard_create(void) 17 | { 18 | struct imv_keyboard *keyboard = calloc(1, sizeof *keyboard); 19 | keyboard->context = xkb_context_new(0); 20 | assert(keyboard->context); 21 | 22 | struct xkb_rule_names names = { 23 | .rules = NULL, 24 | .model = NULL, 25 | .layout = NULL, 26 | .variant = NULL, 27 | .options = NULL, 28 | }; 29 | keyboard->keymap = xkb_keymap_new_from_names(keyboard->context, &names, 0); 30 | assert(keyboard->keymap); 31 | keyboard->state = xkb_state_new(keyboard->keymap); 32 | assert(keyboard->state); 33 | 34 | return keyboard; 35 | } 36 | 37 | void imv_keyboard_free(struct imv_keyboard *keyboard) 38 | { 39 | if (!keyboard) { 40 | return; 41 | } 42 | xkb_state_unref(keyboard->state); 43 | keyboard->state = NULL; 44 | xkb_keymap_unref(keyboard->keymap); 45 | keyboard->keymap = NULL; 46 | xkb_context_unref(keyboard->context); 47 | keyboard->context = NULL; 48 | free(keyboard); 49 | } 50 | 51 | static const int scancode_offset = 8; 52 | 53 | void imv_keyboard_update_key(struct imv_keyboard *keyboard, int scancode, bool pressed) 54 | { 55 | xkb_state_update_key(keyboard->state, scancode + scancode_offset, pressed ? XKB_KEY_DOWN : XKB_KEY_UP); 56 | } 57 | 58 | void imv_keyboard_update_mods(struct imv_keyboard *keyboard, 59 | int depressed, int latched, int locked) 60 | { 61 | xkb_state_update_mask(keyboard->state, depressed, latched, locked, 0, 0, 0); 62 | } 63 | 64 | static const char *describe_prefix(struct imv_keyboard *keyboard) 65 | { 66 | const bool ctrl = (xkb_state_mod_name_is_active(keyboard->state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE) > 0); 67 | const bool alt = (xkb_state_mod_name_is_active(keyboard->state, XKB_MOD_NAME_ALT, XKB_STATE_MODS_EFFECTIVE) > 0); 68 | const bool shift = (xkb_state_mod_name_is_active(keyboard->state, XKB_MOD_NAME_SHIFT, XKB_STATE_MODS_EFFECTIVE) > 0); 69 | 70 | if (ctrl && !alt && !shift) { 71 | return "Ctrl+"; 72 | } else if (!ctrl && alt && !shift) { 73 | return "Meta+"; 74 | } else if (!ctrl && !alt && shift) { 75 | return "Shift+"; 76 | } else if (ctrl && alt && !shift) { 77 | return "Ctrl+Meta+"; 78 | } else if (ctrl && !alt && shift) { 79 | return "Ctrl+Shift+"; 80 | } else if (!ctrl && alt && shift) { 81 | return "Meta+Shift+"; 82 | } else if (ctrl && alt && shift) { 83 | return "Ctrl+Meta+Shift+"; 84 | } else { 85 | return ""; 86 | } 87 | } 88 | 89 | size_t imv_keyboard_keyname(struct imv_keyboard *keyboard, int scancode, char *buf, size_t buflen) 90 | { 91 | xkb_keysym_t keysym = xkb_state_key_get_one_sym(keyboard->state, scancode + scancode_offset); 92 | return xkb_keysym_get_name(keysym, buf, buflen); 93 | } 94 | 95 | char *imv_keyboard_describe_key(struct imv_keyboard *keyboard, int scancode) 96 | { 97 | char keyname[128] = {0}; 98 | imv_keyboard_keyname(keyboard, scancode, keyname, sizeof keyname); 99 | 100 | /* Modifier keys don't count on their own, only when pressed with another key */ 101 | if (!strcmp(keyname, "Control_L") || 102 | !strcmp(keyname, "Control_R") || 103 | !strcmp(keyname, "Alt_L") || 104 | !strcmp(keyname, "Alt_R") || 105 | !strcmp(keyname, "Shift_L") || 106 | !strcmp(keyname, "Shift_R") || 107 | !strcmp(keyname, "Meta_L") || 108 | !strcmp(keyname, "Meta_R") || 109 | !strcmp(keyname, "Super_L") || 110 | !strcmp(keyname, "Super_R")) { 111 | return NULL; 112 | } 113 | 114 | const char *prefix = describe_prefix(keyboard); 115 | 116 | char buf[128]; 117 | snprintf(buf, sizeof buf, "%s%s", prefix, keyname); 118 | return strdup(buf); 119 | } 120 | 121 | size_t imv_keyboard_get_text(struct imv_keyboard *keyboard, int scancode, char *buf, size_t buflen) 122 | { 123 | return xkb_state_key_get_utf8(keyboard->state, scancode + scancode_offset, buf, buflen); 124 | } 125 | 126 | void imv_keyboard_set_keymap(struct imv_keyboard *keyboard, const char *keymap) 127 | { 128 | xkb_keymap_unref(keyboard->keymap); 129 | keyboard->keymap = xkb_keymap_new_from_string(keyboard->context, keymap, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); 130 | xkb_state_unref(keyboard->state); 131 | keyboard->state = xkb_state_new(keyboard->keymap); 132 | } 133 | 134 | bool imv_keyboard_should_key_repeat(struct imv_keyboard *keyboard, int scancode) 135 | { 136 | return xkb_keymap_key_repeats(keyboard->keymap, scancode + scancode_offset); 137 | } 138 | -------------------------------------------------------------------------------- /src/backend_libtiff.c: -------------------------------------------------------------------------------- 1 | #include "backend.h" 2 | #include "bitmap.h" 3 | #include "image.h" 4 | #include "source.h" 5 | #include "source_private.h" 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | struct private { 12 | TIFF *tiff; 13 | void *data; 14 | size_t pos, len; 15 | int width; 16 | int height; 17 | }; 18 | 19 | static tsize_t mem_read(thandle_t data, tdata_t buffer, tsize_t len) 20 | { 21 | struct private *private = (struct private*)data; 22 | memcpy(buffer, (char*)private->data + private->pos, len); 23 | private->pos += len; 24 | return len; 25 | } 26 | 27 | static tsize_t mem_write(thandle_t data, tdata_t buffer, tsize_t len) 28 | { 29 | struct private *private = (struct private*)data; 30 | memcpy((char*)private->data + private->pos, buffer, len); 31 | private->pos += len; 32 | return len; 33 | } 34 | 35 | static int mem_close(thandle_t data) 36 | { 37 | (void)data; 38 | return 0; 39 | } 40 | 41 | static toff_t mem_seek(thandle_t data, toff_t pos, int whence) 42 | { 43 | struct private *private = (struct private*)data; 44 | if (whence == SEEK_SET) { 45 | private->pos = pos; 46 | } else if (whence == SEEK_CUR) { 47 | private->pos += pos; 48 | } else if (whence == SEEK_END) { 49 | private->pos = private->len + pos; 50 | } else { 51 | return -1; 52 | } 53 | return private->pos; 54 | } 55 | 56 | static toff_t mem_size(thandle_t data) 57 | { 58 | struct private *private = (struct private*)data; 59 | return private->len; 60 | } 61 | 62 | static void free_private(void *raw_private) 63 | { 64 | if (!raw_private) { 65 | return; 66 | } 67 | 68 | struct private *private = raw_private; 69 | TIFFClose(private->tiff); 70 | private->tiff = NULL; 71 | 72 | free(private); 73 | } 74 | 75 | static void load_image(void *raw_private, struct imv_image **image, int *frametime) 76 | { 77 | *image = NULL; 78 | *frametime = 0; 79 | 80 | struct private *private = raw_private; 81 | 82 | /* libtiff suggests using their own allocation routines to support systems 83 | * with segmented memory. I have no desire to support that, so I'm just 84 | * going to use vanilla malloc/free. Systems where that isn't acceptable 85 | * don't have upstream support from imv. 86 | */ 87 | void *bitmap = malloc(private->height * private->width * 4); 88 | int rcode = TIFFReadRGBAImageOriented(private->tiff, private->width, private->height, 89 | bitmap, ORIENTATION_TOPLEFT, 0); 90 | 91 | /* 1 = success, unlike the rest of *nix */ 92 | if (rcode != 1) { 93 | return; 94 | } 95 | 96 | struct imv_bitmap *bmp = malloc(sizeof *bmp); 97 | bmp->width = private->width; 98 | bmp->height = private->height; 99 | bmp->format = IMV_ABGR; 100 | bmp->data = bitmap; 101 | *image = imv_image_create_from_bitmap(bmp); 102 | } 103 | 104 | static const struct imv_source_vtable vtable = { 105 | .load_first_frame = load_image, 106 | .free = free_private 107 | }; 108 | 109 | static enum backend_result open_path(const char *path, struct imv_source **src) 110 | { 111 | struct private private; 112 | 113 | TIFFSetErrorHandler(NULL); 114 | 115 | private.tiff = TIFFOpen(path, "r"); 116 | if (!private.tiff) { 117 | /* Header is read, so no BAD_PATH check here */ 118 | return BACKEND_UNSUPPORTED; 119 | } 120 | 121 | TIFFGetField(private.tiff, TIFFTAG_IMAGEWIDTH, &private.width); 122 | TIFFGetField(private.tiff, TIFFTAG_IMAGELENGTH, &private.height); 123 | 124 | struct private *new_private = malloc(sizeof private); 125 | memcpy(new_private, &private, sizeof private); 126 | 127 | *src = imv_source_create(&vtable, new_private); 128 | return BACKEND_SUCCESS; 129 | } 130 | 131 | static enum backend_result open_memory(void *data, size_t len, struct imv_source **src) 132 | { 133 | TIFFSetErrorHandler(NULL); 134 | struct private *private = malloc(sizeof *private); 135 | private->data = data; 136 | private->len = len; 137 | private->pos = 0; 138 | private->tiff = TIFFClientOpen("-", "rm", (thandle_t)private, 139 | &mem_read, &mem_write, &mem_seek, &mem_close, &mem_size, 140 | NULL, NULL); 141 | if (!private->tiff) { 142 | /* Header is read, so no BAD_PATH check here */ 143 | free(private); 144 | return BACKEND_UNSUPPORTED; 145 | } 146 | 147 | TIFFGetField(private->tiff, TIFFTAG_IMAGEWIDTH, &private->width); 148 | TIFFGetField(private->tiff, TIFFTAG_IMAGELENGTH, &private->height); 149 | 150 | *src = imv_source_create(&vtable, private); 151 | return BACKEND_SUCCESS; 152 | } 153 | 154 | const struct imv_backend imv_backend_libtiff = { 155 | .name = "libtiff", 156 | .description = "The de-facto tiff library", 157 | .website = "http://www.libtiff.org/", 158 | .license = "MIT", 159 | .open_path = &open_path, 160 | .open_memory = &open_memory, 161 | }; 162 | -------------------------------------------------------------------------------- /doc/imv.5.txt: -------------------------------------------------------------------------------- 1 | ///// 2 | vim:set ts=4 sw=4 tw=82 noet: 3 | ///// 4 | :quotes.~: 5 | 6 | imv (5) 7 | ======= 8 | 9 | Name 10 | ---- 11 | imv - imv configuration file 12 | 13 | Description 14 | ----------- 15 | 16 | imv can be customised with this configuration file, changing its default 17 | behaviour, key bindings, and appearance. 18 | 19 | The imv configuration file is an ini-style file, with multiple 'key = value' 20 | settings, separated into several '[section]'s. 21 | 22 | Options 23 | ------- 24 | 25 | The *[options]* section accepts the following settings: 26 | 27 | *background* = :: 28 | Set the background in imv. Can either be a 6-digit hexadecimal colour code, 29 | or 'checks' for a chequered background. Defaults to '000000' 30 | 31 | *fullscreen* = :: 32 | Start imv fullscreen. Defaults to 'false'. 33 | 34 | *width* = :: 35 | Initial width of the imv window. Defaults to 1280. 36 | 37 | *height* = :: 38 | Initial height of the imv window. Defaults to 720. 39 | 40 | *initial_pan* = :: 41 | Initial pan/focus position factor of the opened images. A value of 50 42 | represents the middle point of the image (50%). 43 | Defaults to '50 50' 44 | 45 | *list_files_at_exit* = :: 46 | Print open files to stdout at exit, each on a separate line. 47 | Defaults to 'false'. 48 | 49 | *loop_input* = :: 50 | Return to first image after viewing the last one. Defaults to 'true'. 51 | 52 | *overlay* = :: 53 | Start with the overlay visible. Defaults to 'false'. 54 | 55 | *overlay_font* = :: 56 | Use the specified font in the overlay. Defaults to 'Monospace:24'. 57 | 58 | *overlay_text* = :: 59 | Use the given text as the overlay's text. The provided text is shell expanded, 60 | so the output of commands can be used (for example, '$(ls)'). Environment 61 | variables can also be used, including the ones accessible to imv's 'exec' 62 | command. 63 | 64 | *overlay_text_color* = :: 65 | Set the color for the text in the overlay. Is a 6-digit hexadecimal color 66 | code. Defaults to 'ffffff'. 67 | 68 | *overlay_text_alpha* = :: 69 | Set the alpha for the text in the overlay. Is a 2-digit hexadecimal color 70 | code. Defaults to 'ff'. 71 | 72 | *overlay_background_color* = :: 73 | Set the color for the background of the overlay. Is a 6-digit hexadecimal color 74 | code. Defaults to '000000'. 75 | 76 | *overlay_background_alpha* = :: 77 | Set the alpha for the background of the overlay. Is a 2-digit hexadecimal color 78 | code. Defaults to 'c3'. 79 | 80 | *overlay_position_bottom* = :: 81 | Display the overlay at the bottom of the imv window, instead of the top. 82 | 83 | *recursively* = :: 84 | Load input paths recursively. Defaults to 'false'. 85 | 86 | *scaling_mode* = :: 87 | Set scaling mode to use. 'none' will show each image at its actual size. 88 | 'shrink' will scale down the image to fit inside the window. 'full' will 89 | both scale up and scale down the image to fit perfectly inside the window. 90 | 'crop' will scale and crop the image to fill the window. 91 | Defaults to 'full'. 92 | 93 | *slideshow_duration* = :: 94 | Start imv in slideshow mode, and set the amount of time to show each image 95 | for in seconds. Defaults to '0', i.e. no slideshow. 96 | 97 | *suppress_default_binds* = :: 98 | Disable imv's built-in binds so they don't conflict with custom ones. 99 | Defaults to 'false'. 100 | 101 | *title_text* = :: 102 | Use the given text as the window's title. The provided text is shell 103 | expanded, so the output of commands can be used: '$(ls)' as can environment 104 | variables, including the ones accessible to imv's 'exec' command. 105 | 106 | *upscaling_method* = :: 107 | Use the specified method to upscale images. Defaults to 'linear'. 108 | 109 | Aliases 110 | ------- 111 | 112 | The *[aliases]* section allows aliases to be added for imv's build in commands. 113 | For example, 'x = close' would add a 'x' command that simply executes the 114 | 'close' command. Any arguments provided to an alias are appended to the 115 | command configured by the alias. 116 | 117 | Binds 118 | ----- 119 | 120 | The *[binds]* section allows custom key bindings to be added to imv. 121 | 122 | Binds are in the format 'key combination = command'. A key combination can 123 | consist of multiple keys in succession. Multiple commands for a single key 124 | combination can be defined by separating each command with a ';'. Single and 125 | double quotes are honoured, as is escaping with a backslash, to allow the 126 | proper quoting of shell commands. 127 | 128 | Single keys such as 'q' are just that: 'q = quit' will bind the 'q' key to the 129 | 'quit' command. 130 | 131 | Modifier keys can be specified by prefixing them: 'Ctrl+q', 'Meta+f', 132 | 'Shift+G'. If multiple modifier keys are desired, they are specified in the 133 | order 'Ctrl+Meta+Shift'. When a key's name is more than a single character, 134 | or a modifier is used it must be wrapped in '<' and '>', for example: 135 | ''. 136 | 137 | Multiple keys in succession can be specified by listing them in order: 138 | 'gg = goto 1' will bind two presses of the 'g' key to jump to the first 139 | image, and 'p = exec echo hi' will bind the key sequence of 'Ctrl+a' 140 | followed by 'p' to executing the shell command 'echo hi'. 141 | 142 | Many keys, such as '<', and '>' have special names. On X11, these can be easily 143 | found with the xev(1) command. For example, '!' is called 'exclam', '<' is 144 | called 'less', '>' is called 'greater'. 145 | 146 | A complete list of keysyms can also be found on most systems with the 147 | 'dumpkeys -l' command. 148 | 149 | **imv**(1) 150 | -------------------------------------------------------------------------------- /src/backend_libnsgif.c: -------------------------------------------------------------------------------- 1 | #include "backend.h" 2 | #include "bitmap.h" 3 | #include "image.h" 4 | #include "log.h" 5 | #include "source.h" 6 | #include "source_private.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | struct private { 16 | int current_frame; 17 | gif_animation gif; 18 | void *data; 19 | size_t len; 20 | }; 21 | 22 | static void* bitmap_create(int width, int height) 23 | { 24 | const size_t bytes_per_pixel = 4; 25 | return calloc(width * height, bytes_per_pixel); 26 | } 27 | 28 | static void bitmap_destroy(void *bitmap) 29 | { 30 | free(bitmap); 31 | } 32 | 33 | static unsigned char* bitmap_get_buffer(void *bitmap) 34 | { 35 | return bitmap; 36 | } 37 | 38 | static void bitmap_set_opaque(void *bitmap, bool opaque) 39 | { 40 | (void)bitmap; 41 | (void)opaque; 42 | } 43 | 44 | static bool bitmap_test_opaque(void *bitmap) 45 | { 46 | (void)bitmap; 47 | return false; 48 | } 49 | 50 | static void bitmap_mark_modified(void *bitmap) 51 | { 52 | (void)bitmap; 53 | } 54 | 55 | static gif_bitmap_callback_vt bitmap_callbacks = { 56 | bitmap_create, 57 | bitmap_destroy, 58 | bitmap_get_buffer, 59 | bitmap_set_opaque, 60 | bitmap_test_opaque, 61 | bitmap_mark_modified 62 | }; 63 | 64 | 65 | static void free_private(void *raw_private) 66 | { 67 | if (!raw_private) { 68 | return; 69 | } 70 | 71 | struct private *private = raw_private; 72 | gif_finalise(&private->gif); 73 | munmap(private->data, private->len); 74 | free(private); 75 | } 76 | 77 | static void push_current_image(struct private *private, 78 | struct imv_image **image, int *frametime) 79 | { 80 | struct imv_bitmap *bmp = malloc(sizeof *bmp); 81 | bmp->width = private->gif.width; 82 | bmp->height = private->gif.height; 83 | bmp->format = IMV_ABGR; 84 | size_t len = 4 * bmp->width * bmp->height; 85 | bmp->data = malloc(len); 86 | memcpy(bmp->data, private->gif.frame_image, len); 87 | 88 | *image = imv_image_create_from_bitmap(bmp); 89 | *frametime = private->gif.frames[private->current_frame].frame_delay * 10.0; 90 | } 91 | 92 | static void first_frame(void *raw_private, struct imv_image **image, int *frametime) 93 | { 94 | *image = NULL; 95 | *frametime = 0; 96 | 97 | struct private *private = raw_private; 98 | private->current_frame = 0; 99 | 100 | gif_result code = gif_decode_frame(&private->gif, private->current_frame); 101 | if (code != GIF_OK) { 102 | imv_log(IMV_DEBUG, "libnsgif: failed to decode first frame\n"); 103 | return; 104 | } 105 | 106 | push_current_image(private, image, frametime); 107 | } 108 | 109 | static void next_frame(void *raw_private, struct imv_image **image, int *frametime) 110 | { 111 | *image = NULL; 112 | *frametime = 0; 113 | 114 | struct private *private = raw_private; 115 | 116 | private->current_frame++; 117 | private->current_frame %= private->gif.frame_count; 118 | 119 | gif_result code = gif_decode_frame(&private->gif, private->current_frame); 120 | if (code != GIF_OK) { 121 | imv_log(IMV_DEBUG, "libnsgif: failed to decode a frame\n"); 122 | return; 123 | } 124 | 125 | push_current_image(private, image, frametime); 126 | } 127 | 128 | static const struct imv_source_vtable vtable = { 129 | .load_first_frame = first_frame, 130 | .load_next_frame = next_frame, 131 | .free = free_private 132 | }; 133 | 134 | static enum backend_result open_memory(void *data, size_t len, struct imv_source **src) 135 | { 136 | struct private *private = calloc(1, sizeof *private); 137 | gif_create(&private->gif, &bitmap_callbacks); 138 | 139 | gif_result code; 140 | do { 141 | code = gif_initialise(&private->gif, len, data); 142 | } while (code == GIF_WORKING); 143 | 144 | if (code != GIF_OK) { 145 | gif_finalise(&private->gif); 146 | free(private); 147 | imv_log(IMV_DEBUG, "libsngif: unsupported file\n"); 148 | return BACKEND_UNSUPPORTED; 149 | } 150 | 151 | *src = imv_source_create(&vtable, private); 152 | return BACKEND_SUCCESS; 153 | } 154 | 155 | static enum backend_result open_path(const char *path, struct imv_source **src) 156 | { 157 | imv_log(IMV_DEBUG, "libnsgif: open_path(%s)\n", path); 158 | 159 | int fd = open(path, O_RDONLY); 160 | if (fd < 0) { 161 | return BACKEND_BAD_PATH; 162 | } 163 | 164 | off_t len = lseek(fd, 0, SEEK_END); 165 | if (len < 0) { 166 | close(fd); 167 | return BACKEND_BAD_PATH; 168 | } 169 | 170 | void *data = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0); 171 | close(fd); 172 | if (data == MAP_FAILED || !data) { 173 | return BACKEND_BAD_PATH; 174 | } 175 | 176 | struct private *private = calloc(1, sizeof *private); 177 | private->data = data; 178 | private->len = len; 179 | gif_create(&private->gif, &bitmap_callbacks); 180 | 181 | gif_result code; 182 | do { 183 | code = gif_initialise(&private->gif, private->len, private->data); 184 | } while (code == GIF_WORKING); 185 | 186 | if (code != GIF_OK) { 187 | gif_finalise(&private->gif); 188 | munmap(private->data, private->len); 189 | free(private); 190 | imv_log(IMV_DEBUG, "libsngif: unsupported file\n"); 191 | return BACKEND_UNSUPPORTED; 192 | } 193 | 194 | imv_log(IMV_DEBUG, "libnsgif: num_frames=%d\n", private->gif.frame_count); 195 | imv_log(IMV_DEBUG, "libnsgif: width=%d\n", private->gif.width); 196 | imv_log(IMV_DEBUG, "libnsgif: height=%d\n", private->gif.height); 197 | 198 | *src = imv_source_create(&vtable, private); 199 | return BACKEND_SUCCESS; 200 | } 201 | 202 | 203 | const struct imv_backend imv_backend_libnsgif = { 204 | .name = "libnsgif", 205 | .description = "Tiny GIF decoding library from the NetSurf project", 206 | .website = "https://www.netsurf-browser.org/projects/libnsgif/", 207 | .license = "MIT", 208 | .open_path = &open_path, 209 | .open_memory = &open_memory, 210 | }; 211 | -------------------------------------------------------------------------------- /src/xdg-shell-protocol.c: -------------------------------------------------------------------------------- 1 | /* Generated by wayland-scanner 1.17.0 */ 2 | 3 | /* 4 | * Copyright © 2008-2013 Kristian Høgsberg 5 | * Copyright © 2013 Rafael Antognolli 6 | * Copyright © 2013 Jasper St. Pierre 7 | * Copyright © 2010-2013 Intel Corporation 8 | * Copyright © 2015-2017 Samsung Electronics Co., Ltd 9 | * Copyright © 2015-2017 Red Hat Inc. 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a 12 | * copy of this software and associated documentation files (the "Software"), 13 | * to deal in the Software without restriction, including without limitation 14 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * and/or sell copies of the Software, and to permit persons to whom the 16 | * Software is furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice (including the next 19 | * paragraph) shall be included in all copies or substantial portions of the 20 | * Software. 21 | * 22 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 25 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 28 | * DEALINGS IN THE SOFTWARE. 29 | */ 30 | 31 | #include 32 | #include 33 | #include "wayland-util.h" 34 | 35 | #ifndef __has_attribute 36 | # define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ 37 | #endif 38 | 39 | #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) 40 | #define WL_PRIVATE __attribute__ ((visibility("hidden"))) 41 | #else 42 | #define WL_PRIVATE 43 | #endif 44 | 45 | extern const struct wl_interface wl_output_interface; 46 | extern const struct wl_interface wl_seat_interface; 47 | extern const struct wl_interface wl_surface_interface; 48 | extern const struct wl_interface xdg_popup_interface; 49 | extern const struct wl_interface xdg_positioner_interface; 50 | extern const struct wl_interface xdg_surface_interface; 51 | extern const struct wl_interface xdg_toplevel_interface; 52 | 53 | static const struct wl_interface *types[] = { 54 | NULL, 55 | NULL, 56 | NULL, 57 | NULL, 58 | &xdg_positioner_interface, 59 | &xdg_surface_interface, 60 | &wl_surface_interface, 61 | &xdg_toplevel_interface, 62 | &xdg_popup_interface, 63 | &xdg_surface_interface, 64 | &xdg_positioner_interface, 65 | &xdg_toplevel_interface, 66 | &wl_seat_interface, 67 | NULL, 68 | NULL, 69 | NULL, 70 | &wl_seat_interface, 71 | NULL, 72 | &wl_seat_interface, 73 | NULL, 74 | NULL, 75 | &wl_output_interface, 76 | &wl_seat_interface, 77 | NULL, 78 | }; 79 | 80 | static const struct wl_message xdg_wm_base_requests[] = { 81 | { "destroy", "", types + 0 }, 82 | { "create_positioner", "n", types + 4 }, 83 | { "get_xdg_surface", "no", types + 5 }, 84 | { "pong", "u", types + 0 }, 85 | }; 86 | 87 | static const struct wl_message xdg_wm_base_events[] = { 88 | { "ping", "u", types + 0 }, 89 | }; 90 | 91 | WL_PRIVATE const struct wl_interface xdg_wm_base_interface = { 92 | "xdg_wm_base", 2, 93 | 4, xdg_wm_base_requests, 94 | 1, xdg_wm_base_events, 95 | }; 96 | 97 | static const struct wl_message xdg_positioner_requests[] = { 98 | { "destroy", "", types + 0 }, 99 | { "set_size", "ii", types + 0 }, 100 | { "set_anchor_rect", "iiii", types + 0 }, 101 | { "set_anchor", "u", types + 0 }, 102 | { "set_gravity", "u", types + 0 }, 103 | { "set_constraint_adjustment", "u", types + 0 }, 104 | { "set_offset", "ii", types + 0 }, 105 | }; 106 | 107 | WL_PRIVATE const struct wl_interface xdg_positioner_interface = { 108 | "xdg_positioner", 2, 109 | 7, xdg_positioner_requests, 110 | 0, NULL, 111 | }; 112 | 113 | static const struct wl_message xdg_surface_requests[] = { 114 | { "destroy", "", types + 0 }, 115 | { "get_toplevel", "n", types + 7 }, 116 | { "get_popup", "n?oo", types + 8 }, 117 | { "set_window_geometry", "iiii", types + 0 }, 118 | { "ack_configure", "u", types + 0 }, 119 | }; 120 | 121 | static const struct wl_message xdg_surface_events[] = { 122 | { "configure", "u", types + 0 }, 123 | }; 124 | 125 | WL_PRIVATE const struct wl_interface xdg_surface_interface = { 126 | "xdg_surface", 2, 127 | 5, xdg_surface_requests, 128 | 1, xdg_surface_events, 129 | }; 130 | 131 | static const struct wl_message xdg_toplevel_requests[] = { 132 | { "destroy", "", types + 0 }, 133 | { "set_parent", "?o", types + 11 }, 134 | { "set_title", "s", types + 0 }, 135 | { "set_app_id", "s", types + 0 }, 136 | { "show_window_menu", "ouii", types + 12 }, 137 | { "move", "ou", types + 16 }, 138 | { "resize", "ouu", types + 18 }, 139 | { "set_max_size", "ii", types + 0 }, 140 | { "set_min_size", "ii", types + 0 }, 141 | { "set_maximized", "", types + 0 }, 142 | { "unset_maximized", "", types + 0 }, 143 | { "set_fullscreen", "?o", types + 21 }, 144 | { "unset_fullscreen", "", types + 0 }, 145 | { "set_minimized", "", types + 0 }, 146 | }; 147 | 148 | static const struct wl_message xdg_toplevel_events[] = { 149 | { "configure", "iia", types + 0 }, 150 | { "close", "", types + 0 }, 151 | }; 152 | 153 | WL_PRIVATE const struct wl_interface xdg_toplevel_interface = { 154 | "xdg_toplevel", 2, 155 | 14, xdg_toplevel_requests, 156 | 2, xdg_toplevel_events, 157 | }; 158 | 159 | static const struct wl_message xdg_popup_requests[] = { 160 | { "destroy", "", types + 0 }, 161 | { "grab", "ou", types + 22 }, 162 | }; 163 | 164 | static const struct wl_message xdg_popup_events[] = { 165 | { "configure", "iiii", types + 0 }, 166 | { "popup_done", "", types + 0 }, 167 | }; 168 | 169 | WL_PRIVATE const struct wl_interface xdg_popup_interface = { 170 | "xdg_popup", 2, 171 | 2, xdg_popup_requests, 172 | 2, xdg_popup_events, 173 | }; 174 | 175 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'imv', 3 | ['c'], 4 | version: '4.3.0', 5 | license: 'MIT', 6 | meson_version: '>= 0.47', 7 | default_options: ['buildtype=debugoptimized', 'c_std=c99'], 8 | ) 9 | 10 | version = '@0@'.format(meson.project_version()) 11 | prog_git = find_program('git', required: false) 12 | if prog_git.found() 13 | git_description = run_command([prog_git.path(), 'describe', '--dirty', '--always', '--tags']) 14 | if git_description.returncode() == 0 15 | version = git_description.stdout().strip() 16 | endif 17 | endif 18 | add_project_arguments('-DIMV_VERSION="@0@"'.format(version), language: 'c') 19 | 20 | add_project_arguments('-D_XOPEN_SOURCE=700', language: 'c') 21 | 22 | cc = meson.get_compiler('c') 23 | dep_null = dependency('', required: false) 24 | m_dep = cc.find_library('m', required : false) 25 | 26 | _windows = get_option('windows') 27 | if _windows == 'wayland' 28 | build_wayland = true 29 | build_x11 = false 30 | target_single_ws = true 31 | elif _windows == 'x11' 32 | build_wayland = false 33 | build_x11 = true 34 | target_single_ws = true 35 | else 36 | build_wayland = true 37 | build_x11 = true 38 | target_single_ws = false 39 | endif 40 | 41 | gl_dep = dependency('gl', required: false) 42 | if not gl_dep.found() 43 | # libglvnd fallback for pure-wayland systems 44 | gl_dep = dependency('opengl') 45 | endif 46 | 47 | deps_for_imv = [ 48 | dependency('pangocairo'), 49 | gl_dep, 50 | dependency('threads'), 51 | dependency('xkbcommon'), 52 | dependency('icu-io'), 53 | dependency('inih', fallback : ['inih', 'inih_dep']), 54 | m_dep, 55 | ] 56 | 57 | files_common = files( 58 | 'src/binds.c', 59 | 'src/bitmap.c', 60 | 'src/canvas.c', 61 | 'src/commands.c', 62 | 'src/console.c', 63 | 'src/image.c', 64 | 'src/imv.c', 65 | 'src/ipc.c', 66 | 'src/ipc_common.c', 67 | 'src/keyboard.c', 68 | 'src/list.c', 69 | 'src/log.c', 70 | 'src/navigator.c', 71 | 'src/source.c', 72 | 'src/viewport.c', 73 | ) 74 | 75 | files_imv = files_common + files( 76 | 'src/main.c', 77 | ) 78 | 79 | enabled_window_systems = [] 80 | 81 | if build_wayland 82 | files_wayland = files('src/wl_window.c', 'src/xdg-shell-protocol.c') 83 | deps_for_wayland = [ 84 | dependency('wayland-client'), 85 | dependency('wayland-egl'), 86 | dependency('egl'), 87 | cc.find_library('rt'), 88 | ] 89 | enabled_window_systems += 'wayland' 90 | else 91 | deps_for_wayland = dep_null 92 | endif 93 | 94 | if build_x11 95 | files_x11 = files('src/x11_window.c') 96 | deps_for_x11 = [ 97 | dependency('x11'), 98 | dependency('gl'), 99 | dependency('glu'), 100 | dependency('xcb'), 101 | dependency('xkbcommon-x11'), 102 | ] 103 | enabled_window_systems += 'x11' 104 | else 105 | deps_for_x11 = dep_null 106 | endif 107 | 108 | files_msg = files('src/imv_msg.c', 'src/ipc_common.c') 109 | 110 | enabled_backends = [] 111 | foreach backend : [ 112 | ['freeimage', 'library', 'freeimage'], 113 | ['libtiff', 'dependency', 'libtiff-4', []], 114 | ['libpng', 'dependency', 'libpng', []], 115 | ['libjpeg', 'dependency', 'libturbojpeg', []], 116 | ['librsvg', 'dependency', 'librsvg-2.0', '>= 2.44'], 117 | ['libnsgif', 'dependency', 'libnsgif', []], 118 | ['libheif', 'dependency', 'libheif', []], 119 | ] 120 | _backend_name = backend[0] 121 | _dep_type = backend[1] 122 | _dep_name = backend[2] 123 | 124 | if _dep_type == 'dependency' 125 | _dep = dependency(_dep_name, required: get_option(_backend_name), version: backend[3]) 126 | elif _dep_type == 'library' 127 | _dep = cc.find_library(_dep_name, required: get_option(_backend_name)) 128 | else 129 | error('invalid dep type: @0@'.format(_dep_type)) 130 | endif 131 | 132 | if _dep.found() 133 | deps_for_imv += _dep 134 | files_imv += files('src/backend_@0@.c'.format(_backend_name)) 135 | add_project_arguments('-DIMV_BACKEND_@0@'.format(_backend_name.to_upper()), language: 'c') 136 | enabled_backends += _backend_name 137 | endif 138 | endforeach 139 | 140 | executable( 141 | 'imv-msg', 142 | [files_common, files('src/imv_msg.c', 'src/dummy_window.c')], 143 | dependencies: deps_for_imv, 144 | install: true, 145 | install_dir: get_option('bindir'), 146 | ) 147 | 148 | foreach ws : ['wayland', 'x11'] 149 | if get_variable('build_' + ws) 150 | executable( 151 | target_single_ws ? 'imv' : 'imv-@0@'.format(ws), 152 | [get_variable('files_' + ws), files_imv], 153 | dependencies: [deps_for_imv, get_variable('deps_for_' + ws)], 154 | install: true, 155 | install_dir: get_option('bindir'), 156 | ) 157 | endif 158 | endforeach 159 | 160 | if get_option('contrib-commands') 161 | install_data( 162 | files('contrib/imv-dir'), 163 | install_dir: get_option('bindir'), 164 | install_mode: 'rwxr-xr-x', 165 | ) 166 | endif 167 | 168 | if not target_single_ws 169 | install_data( 170 | files('files/imv'), 171 | install_dir: get_option('bindir'), 172 | install_mode: 'rwxr-xr-x', 173 | ) 174 | endif 175 | 176 | desktop_list = [ 177 | 'imv', 178 | ] 179 | if get_option('contrib-commands') 180 | desktop_list += [ 181 | 'imv-dir', 182 | ] 183 | endif 184 | foreach desktop: desktop_list 185 | install_data( 186 | files('files/@0@.desktop'.format(desktop)), 187 | install_dir: '@0@/applications'.format(get_option('datadir')), 188 | install_mode: 'rw-r--r--', 189 | ) 190 | endforeach 191 | 192 | install_data( 193 | files('files/imv_config'), 194 | install_dir: get_option('sysconfdir'), 195 | install_mode: 'rw-r--r--', 196 | ) 197 | 198 | dep_cmocka = dependency('cmocka', required: get_option('test')) 199 | 200 | if dep_cmocka.found() 201 | foreach test : ['list', 'navigator'] 202 | test( 203 | 'test_@0@'.format(test), 204 | executable( 205 | 'test_@0@'.format(test), 206 | [files('test/@0@.c'.format(test), 'src/dummy_window.c'), files_common], 207 | include_directories: include_directories('src'), 208 | dependencies: [deps_for_imv, dep_cmocka], 209 | ) 210 | ) 211 | endforeach 212 | endif 213 | 214 | prog_a2x = find_program('a2x', required: get_option('man')) 215 | 216 | if prog_a2x.found() 217 | man_list = [ 218 | [1, 'imv'], 219 | [1, 'imv-msg'], 220 | [5, 'imv'], 221 | ] 222 | if get_option('contrib-commands') 223 | man_list += [ 224 | [1, 'imv-dir'], 225 | ] 226 | endif 227 | 228 | foreach man : man_list 229 | _section = man[0] 230 | _topic = man[1] 231 | custom_target( 232 | '@0@(@1@)'.format(_topic, _section), 233 | input: 'doc/@0@.@1@.txt'.format(_topic, _section), 234 | output: '@0@.@1@'.format(_topic, _section), 235 | command: [ 236 | prog_a2x, 237 | '--no-xmllint', 238 | '--doctype', 'manpage', 239 | '--format', 'manpage', 240 | '--destination-dir', meson.current_build_dir(), 241 | '@INPUT@' 242 | ], 243 | install: true, 244 | install_dir: '@0@/man@1@'.format(get_option('mandir'), _section) 245 | ) 246 | endforeach 247 | endif 248 | 249 | message('\n\n' 250 | + 'Building imv @0@\n\n'.format(meson.project_version()) 251 | + 'Window systems enabled:\n- ' + '\n- '.join(enabled_window_systems) + '\n\n' 252 | + 'Backends enabled:\n- ' + '\n- '.join(enabled_backends) + '\n' 253 | ) 254 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | imv Changelog 2 | ============= 3 | 4 | v4.3.0 - 2021-08-05 5 | 6 | * Give freeimage backend preceedence over libjpeg 7 | * Fix lack of X11 window deletion event handling 8 | * Scale fonts correctly for high-DPI wayland 9 | * Suppress annoying TIFF error messages 10 | * Fix missing -lm dependency for imv-msg 11 | * Fix image skipping when reading slow filesystems 12 | * Add a contrib directory 13 | * Add contrib/imv-folder 14 | * Fix zoom speed dependence on buffer/image width 15 | * Improve stat error handling when loading directories 16 | * Fix double-free when backend libjpeg is used 17 | * Fix -n option not working for symbolic links 18 | * List image/heif as a supported mime type 19 | * Fix flickering bug in high-DPI wayland 20 | 21 | v4.2.0 - 2020-12-17 22 | 23 | * Switched to meson build system 24 | * Added HEIF backend 25 | * Added rotate and flip commands 26 | * Added colour support for overlay 27 | * Added some missing command/config documentation 28 | * Read directories in alphabetical order 29 | * Fixed several memory leaks 30 | * Improved compatibility with non glibc systems 31 | * Fixed several window management bugs for Wayland 32 | * Fixed bug when selecting first/last image 33 | 34 | v4.1.0 - 2019-12-21 35 | 36 | * Added libsngif backend 37 | * Added command history to console 38 | * Added proper UTF-8 support to the console 39 | * Fixed name of libjpeg-turbo backend 40 | * Fixed reading images from stdin 41 | * Increased maximum supported PNG file size 42 | * Added Keywords to imv.desktop 43 | * Set NoDisplay in imv.desktop 44 | * Simplified async logic in backends 45 | * Fixed typo in manpages 46 | * Fixed name of the LGPL 47 | 48 | v4.0.1 - 2019-08-28 49 | 50 | * Documented default binds in man page 51 | * Added icon to imv.desktop 52 | * Added builtin aliases to provide backwards compatibility for removed commands 53 | * Added warning when legacy bind syntax is detected 54 | 55 | v4.0.0 - 2019-08-27 56 | 57 | BREAKING CHANGES: 58 | * Fixed keyboard layout handling, changing bind syntax 59 | * Renamed many commands (select_rel -> next/prev, select_abs -> goto, etc.) 60 | * Temporarily removed autoresize option 61 | 62 | * Dropped SDL2 dependency, implementing Wayland and X11 support natively 63 | through imv-wayland and imv-x11 binaries 64 | * Added support for displaying SVGs at native resolution regardless of zoom 65 | level 66 | * Added hidpi support on Wayland 67 | * Added '-c' argument to specify commands to run at startup 68 | * Added 'bind' command to add new binds at runtime 69 | * Added 'background' command to change background colour at runtime 70 | * Added 'upscaling' command to modify upscaling method at runtime 71 | * Added optional argument to close command to specify an index or all images 72 | * Added initial_pan option to configure which part of an image is initially 73 | focused on 74 | * Added support for aliases passing arguments to underlying their commands 75 | * Added imv-msg program to send commands to a running instance of imv 76 | * Added $imv_pid environment variable 77 | * Allowed imv to remain open with no images open 78 | * Improved unicode support in overlay 79 | * Fixed typo in $imv_slideshow_duration environment variable 80 | * Added new crop scaling method, which will zoom in until an image completely 81 | fills the window 82 | * Fixed a bug where 16-bit greyscale images would not load 83 | * Fixed a memory corruption bug in generic list implementation 84 | * Fixed several memory leaks 85 | 86 | v3.1.2 - 2019-06-24 87 | 88 | * Fix manpage packaging regression introduced in v3.1.1 89 | 90 | v3.1.1 - 2019-06-22 91 | 92 | * Adjusted Makefile to improve packaging on BSDs 93 | 94 | v3.1.0 - 2019-06-17 95 | 96 | * Added support for multiple image loaders, allowing imv to use libraries other 97 | than FreeImage. This adds support for SVGs, and in the future, other formats 98 | as required. 99 | * Loaders added for libpng, libtiff, librsvg, libturbojpeg. 100 | * Added support for binding multiple commands to a single key 101 | * Support for hidpi rendering with SDL >= 2.0.10 102 | * Added -v flag to show version number 103 | * Allow 'Escape' to be bound, changing bind abort sequence to 2x'Escape' 104 | * Fixed bug where path list from stdin would sometime be truncated 105 | * New releases only published under the MIT license, with FreeImage optionally 106 | used under the FIPL 107 | * Fixed several memory leaks 108 | * Miscellaneous code cleanup and documentation fixes 109 | 110 | v3.0.0 - 2018-05-08 111 | 112 | BREAKING CHANGES: 113 | * Change a,s,S flags to -s syntax 114 | * Make -u take an argument 115 | 116 | 117 | * Dual-license under MIT and GPL 118 | * Large refactor of entire codebase 119 | * Added config file 120 | * Added command system 121 | * Added bind system 122 | * Improved power consumption by sleeping more opportunistically 123 | * Show image scale in window title by default 124 | * Prevent scrolling images offscreen 125 | * Fix bug where slideshow timer is not reset when an image is closed 126 | 127 | v2.1.3 - 2016-10-22 128 | 129 | * Fix various resource leaks 130 | * Fix a bug where imv would to try and catch up on long periods of lost gif 131 | playback time if it were suspended for a second or more. 132 | * Improve handling of unusual read() return codes 133 | * Fix a linking issue with unit tests on some platforms 134 | 135 | v2.1.2 - 2016-06-15 136 | ------------------- 137 | 138 | * Fix build issues introduced by v2.1.1 139 | 140 | v2.1.1 - 2016-05-12 141 | ------------------- 142 | 143 | * Fix compatibility with older gcc versions 144 | * Fix bug where unchanged files were unnecessarily reloaded 145 | * Fix bug where first frame of gif or first slide was changed to quickly 146 | * Minor performance improvements 147 | 148 | v2.1.0 - 2016-04-27 149 | ------------------- 150 | 151 | * Add `-x` option to quit imv when end of files reached 152 | * Honour EXIF rotation information in JPGs 153 | * Read file list from stdin continuously 154 | * Fix fullscreen bug with i3 155 | * Fix bug where gifs would sometimes not auto-scale when opened 156 | * Add commit hash to version string of non-release builds 157 | * Fix bug where '+' did not work on foriegn keyboard layouts 158 | 159 | v2.0.0 - 2016-02-08 160 | ------------------- 161 | 162 | BREAKING CHANGE: 163 | * When no arguments given, read paths from stdin 164 | * When '-' given as an argument, read file data from stdin 165 | 166 | 167 | * Use 'Monospace' as default font 168 | * Fixed some types of animated gifs that did not display correctly 169 | * Added third scaling mode: 'best fit' where images will be scaled down to fit, 170 | but *not* scaled up to fill the window 171 | * Trimmed output of '-h', making the manpage the single source of truth 172 | * Improvements to documentation 173 | * Improved portability across platforms 174 | * Allow non-integral slideshow times 175 | * Improved test coverage 176 | * Various improvements to the build 177 | - `uninstall` target added to Makefile 178 | - `V` option added to Makefile 179 | - Respect `PREFIX`, `CFLAGS`, and `LDFLAGS` 180 | 181 | v1.2.0 - 2015-12-11 182 | ------------------- 183 | 184 | * Added a text overlay (-d to enable, 'd' to toggle) 185 | - Font used is configurable with -e option 186 | * Added slideshow support (-t option) 187 | * Added -l option to list all open images on exit 188 | * Automatically reload images if they change on disk 189 | * Moved image loading into background thread to improve UI responsiveness 190 | * Auto hide the mouse when appropriate 191 | * Added support for upper and lower case hex in the -b option. 192 | * Fixed a couple of crashes 193 | 194 | v1.1.0 - 2015-11-14 195 | ------------------- 196 | 197 | * Relicensed to GPL 198 | * Added support for transparency 199 | * Added 'p' hotkey to print the current image's path to stdout 200 | * Added '-n' option to start at a specific image 201 | * Added '-b' option to set the background 202 | * Added '-u' option to set resampling to nearest-neighbour 203 | * Changed '-i' option to '-' for reading paths from stdin 204 | * Added a .desktop file, for xdg-open support 205 | * Fixed compilation on Fedora 22 206 | * Fixed crash when using SDL's software renderer 207 | * Fixed bug where single frame gifs would not be rendered 208 | * Fixed animated gif playback speed for some unusual gifs 209 | * Fixed slow zoom speed on large images 210 | * Fixed a memory leak 211 | * Added a FreeImage copyright notice for GPL compliance 212 | 213 | v1.0.0 - 2015-11-11 214 | ------------------- 215 | Intitial release 216 | -------------------------------------------------------------------------------- /src/console.c: -------------------------------------------------------------------------------- 1 | #include "console.h" 2 | 3 | #include "list.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | struct imv_console { 13 | char *buffer; 14 | size_t buffer_len; 15 | size_t cursor; 16 | 17 | imv_console_callback callback; 18 | void *callback_data; 19 | 20 | struct list *history; 21 | ssize_t history_item; /* -1 when not traversing history */ 22 | char *history_before; /* contents of line before history was opened */ 23 | }; 24 | 25 | /* Iterates forwards over characters in a UTF-8 string */ 26 | static size_t next_char(char *buffer, size_t position) 27 | { 28 | size_t result = position; 29 | UErrorCode status = U_ZERO_ERROR; 30 | UText *ut = utext_openUTF8(NULL, buffer, -1, &status); 31 | 32 | UBreakIterator *it = ubrk_open(UBRK_CHARACTER, NULL, NULL, 0, &status); 33 | ubrk_setUText(it, ut, &status); 34 | 35 | int boundary = ubrk_following(it, (int)position); 36 | if (boundary != UBRK_DONE) { 37 | result = (size_t)boundary; 38 | } 39 | 40 | ubrk_close(it); 41 | 42 | utext_close(ut); 43 | assert(U_SUCCESS(status)); 44 | return result; 45 | } 46 | 47 | /* Iterates backwards over characters in a UTF-8 string */ 48 | static size_t prev_char(char *buffer, size_t position) 49 | { 50 | size_t result = position; 51 | UErrorCode status = U_ZERO_ERROR; 52 | UText *ut = utext_openUTF8(NULL, buffer, -1, &status); 53 | 54 | UBreakIterator *it = ubrk_open(UBRK_CHARACTER, NULL, NULL, 0, &status); 55 | ubrk_setUText(it, ut, &status); 56 | 57 | int boundary = ubrk_preceding(it, (int)position); 58 | if (boundary != UBRK_DONE) { 59 | result = (size_t)boundary; 60 | } 61 | 62 | ubrk_close(it); 63 | 64 | utext_close(ut); 65 | assert(U_SUCCESS(status)); 66 | return result; 67 | } 68 | 69 | static void add_to_history(struct list *history, const char *line) 70 | { 71 | /* Don't add blank lines */ 72 | if (line[0] == '\0') { 73 | return; 74 | } 75 | 76 | /* Don't add the same line consecutively */ 77 | if (history->len > 0 && !strcmp(history->items[history->len - 1], line)) { 78 | return; 79 | } 80 | 81 | list_append(history, strdup(line)); 82 | } 83 | 84 | static void history_back(struct imv_console *console) 85 | { 86 | if (console->history->len < 1) { 87 | /* No history to browse */ 88 | return; 89 | } 90 | 91 | if (console->history_item == 0) { 92 | /* At the top of history. Do nothing. */ 93 | return; 94 | } 95 | 96 | if (console->history_item == -1) { 97 | /* Enter history from the bottom */ 98 | free(console->history_before); 99 | console->history_before = strdup(console->buffer); 100 | console->history_item = console->history->len - 1; 101 | } else { 102 | /* Move back in history */ 103 | console->history_item--; 104 | } 105 | 106 | strncpy(console->buffer, 107 | console->history->items[console->history_item], 108 | console->buffer_len); 109 | console->cursor = strlen(console->buffer); 110 | } 111 | 112 | static void history_forward(struct imv_console *console) 113 | { 114 | if (console->history_item == -1) { 115 | /* Not in history, do nothing */ 116 | return; 117 | } 118 | 119 | if ((size_t)console->history_item == console->history->len - 1) { 120 | /* At the end of history, restore the backup */ 121 | strncpy(console->buffer, console->history_before, console->buffer_len); 122 | console->cursor = strlen(console->buffer); 123 | free(console->history_before); 124 | console->history_before = NULL; 125 | console->history_item = -1; 126 | } else { 127 | /* Move forward in history */ 128 | console->history_item++; 129 | strncpy(console->buffer, 130 | console->history->items[console->history_item], 131 | console->buffer_len); 132 | console->cursor = strlen(console->buffer); 133 | } 134 | 135 | } 136 | 137 | struct imv_console *imv_console_create(void) 138 | { 139 | struct imv_console *console = calloc(1, sizeof *console); 140 | console->buffer_len = 1024; 141 | console->history_item = -1; 142 | console->history = list_create(); 143 | return console; 144 | } 145 | 146 | void imv_console_free(struct imv_console *console) 147 | { 148 | free(console->buffer); 149 | list_deep_free(console->history); 150 | free(console->history_before); 151 | free(console); 152 | } 153 | 154 | void imv_console_set_command_callback(struct imv_console *console, 155 | imv_console_callback callback, void *data) 156 | { 157 | console->callback = callback; 158 | console->callback_data = data; 159 | } 160 | 161 | bool imv_console_is_active(struct imv_console *console) 162 | { 163 | return console->buffer != NULL; 164 | } 165 | 166 | void imv_console_activate(struct imv_console *console) 167 | { 168 | if (console->buffer) { 169 | return; 170 | } 171 | 172 | console->buffer = calloc(1, console->buffer_len); 173 | console->cursor = 0; 174 | console->history_item = -1; 175 | } 176 | 177 | void imv_console_input(struct imv_console *console, const char *text) 178 | { 179 | if (!console || !console->buffer) { 180 | return; 181 | } 182 | 183 | /* Don't allow newlines or control characters. */ 184 | if (*text == '\n' || iscntrl(*text)) { 185 | return; 186 | } 187 | 188 | /* Increase buffer size if needed */ 189 | if (strlen(text) + strlen(console->buffer) + 1 > console->buffer_len) { 190 | console->buffer_len *= 2; 191 | console->buffer = realloc(console->buffer, console->buffer_len); 192 | assert(console->buffer); 193 | } 194 | 195 | /* memmove over everything after the cursor right then copy in the new bytes */ 196 | size_t to_insert = strlen(text); 197 | size_t old_cursor = console->cursor; 198 | size_t new_cursor = console->cursor + to_insert; 199 | size_t to_shift = strlen(console->buffer) - old_cursor; 200 | memmove(&console->buffer[new_cursor], &console->buffer[old_cursor], to_shift); 201 | memcpy(&console->buffer[old_cursor], text, to_insert); 202 | console->cursor = new_cursor; 203 | } 204 | 205 | bool imv_console_key(struct imv_console *console, const char *key) 206 | { 207 | if (!console || !console->buffer) { 208 | return false; 209 | } 210 | 211 | if (!strcmp("Escape", key)) { 212 | free(console->buffer); 213 | console->buffer = NULL; 214 | return true; 215 | } 216 | 217 | if (!strcmp("Return", key)) { 218 | if (console->callback) { 219 | console->callback(console->buffer, console->callback_data); 220 | } 221 | add_to_history(console->history, console->buffer); 222 | free(console->buffer); 223 | console->buffer = NULL; 224 | return true; 225 | } 226 | 227 | if (!strcmp("Left", key) || !strcmp("Ctrl+b", key)) { 228 | console->cursor = prev_char(console->buffer, console->cursor); 229 | return true; 230 | } 231 | 232 | if (!strcmp("Right", key) || !strcmp("Ctrl+f", key)) { 233 | console->cursor = next_char(console->buffer, console->cursor); 234 | return true; 235 | } 236 | 237 | if (!strcmp("Up", key) || !strcmp("Ctrl+p", key)) { 238 | history_back(console); 239 | return true; 240 | } 241 | 242 | if (!strcmp("Down", key) || !strcmp("Ctrl+n", key)) { 243 | history_forward(console); 244 | return true; 245 | } 246 | 247 | if (!strcmp("Ctrl+a", key)) { 248 | console->cursor = 0; 249 | return true; 250 | } 251 | 252 | if (!strcmp("Ctrl+e", key)) { 253 | console->cursor = strlen(console->buffer); 254 | return true; 255 | } 256 | 257 | if (!strcmp("BackSpace", key)) { 258 | /* memmove everything after the cursor left */ 259 | size_t new_cursor = prev_char(console->buffer, console->cursor); 260 | size_t old_cursor = console->cursor; 261 | size_t to_shift = strlen(console->buffer) - new_cursor; 262 | memmove(&console->buffer[new_cursor], &console->buffer[old_cursor], to_shift); 263 | console->cursor = new_cursor; 264 | return true; 265 | } 266 | 267 | return false; 268 | } 269 | 270 | const char *imv_console_prompt(struct imv_console *console) 271 | { 272 | return console->buffer; 273 | } 274 | 275 | size_t imv_console_prompt_cursor(struct imv_console *console) 276 | { 277 | return console->cursor; 278 | } 279 | 280 | const char *imv_console_backlog(struct imv_console *console) 281 | { 282 | (void)console; 283 | return NULL; 284 | } 285 | 286 | void imv_console_write(struct imv_console *console, const char *text) 287 | { 288 | (void)console; 289 | (void)text; 290 | } 291 | 292 | void imv_console_add_completion(struct imv_console *console, const char *template) 293 | { 294 | (void)console; 295 | (void)template; 296 | } 297 | -------------------------------------------------------------------------------- /doc/imv.1.txt: -------------------------------------------------------------------------------- 1 | ///// 2 | vim:set ts=4 sw=4 tw=82 noet: 3 | ///// 4 | :quotes.~: 5 | 6 | imv (1) 7 | ======= 8 | 9 | Name 10 | ---- 11 | imv - Image viewer for X11 and Wayland 12 | 13 | Description 14 | ----------- 15 | 16 | imv is an image viewer for X11 and Wayland, aimed at users of tiling window 17 | managers. It supports a wide variety of image file formats, including animated 18 | gif files. imv will automatically reload the current image, if it is changed on 19 | disk. 20 | 21 | Synopsis 22 | -------- 23 | 'imv' [options] [paths...] 24 | 25 | Options 26 | ------- 27 | 28 | *-h*:: 29 | Show help message and quit. 30 | 31 | *-v*:: 32 | Show version and quit. 33 | 34 | *-b* :: 35 | Set the background colour. Can either be a 6-digit hexadecimal colour code 36 | or 'checks' to show a chequered background. 37 | 38 | *-c* :: 39 | Specify a command to be run on launch, after the configuration has been 40 | loaded. Can be used to configure custom keys with the bind command. This 41 | option can be used multiple times. Commands are run in the order that they 42 | have been passed to imv. 43 | 44 | *-d*:: 45 | Start with overlay visible. 46 | 47 | *-f*:: 48 | Start fullscreen. 49 | 50 | *-l*:: 51 | List open files to stdout at exit. 52 | 53 | *-n* :: 54 | Start with the given path, or index selected. 55 | 56 | *-r*:: 57 | Load directories recursively. 58 | 59 | *-s* :: 60 | Set scaling mode to use. 'none' will show each image at its actual size. 61 | 'shrink' will scale down the image to fit inside the window. 'full' will 62 | both scale up and scale down the image to fit perfectly inside the window. 63 | 'crop' will scale and crop the image to fill the window. 64 | Defaults to 'full'. 65 | 66 | *-t* :: 67 | Start in slideshow mode, with each image shown for the given number of 68 | seconds. 69 | 70 | *-u* :: 71 | Set upscaling method used by imv. 72 | 73 | *-x*:: 74 | Disable looping of input paths. 75 | 76 | Commands 77 | -------- 78 | 79 | Commands can be entered by pressing *:*. imv supports the following commands: 80 | 81 | *quit*:: 82 | Quit imv. Aliased to 'q'. 83 | 84 | *pan* :: 85 | Pan the view by the given amounts. 86 | 87 | *next* :: 88 | Move forwards by a given number of images. Aliased to 'n' 89 | 90 | *prev* :: 91 | Move backwards by a given number of images. Aliased to 'p' 92 | 93 | *goto* :: 94 | Select an image by index. '1' is the first image, '2' the second, etc. 95 | The last image can be indexed as '-1', the second last as '-2'. 96 | Aliased to 'g'. 97 | 98 | *zoom* :: 99 | Zoom into the image by the given amount. Negative values zoom out. 100 | 'actual' resets the zoom to 100%, showing the image at its actual size. 101 | Aliased to 'z'. 102 | 103 | *rotate* <'to'|'by'> :: 104 | Rotate image clockwise by/to the given amount in degrees. 105 | 106 | *flip* <'horizontal'|'vertical'>:: 107 | Flip image horizontally/vertically (across vertical/horizontal axis). 108 | 109 | *open* [-r] :: 110 | Add the given paths to the list of open images. If the '-r' option is 111 | specified, do so recursively. Shell expansions may be used. 112 | Aliased to 'o'. 113 | 114 | *close* [index|'all']:: 115 | Close the currently selected image, or the image at the given index, or 116 | all images. 117 | 118 | *fullscreen*:: 119 | Toggle fullscreen. 120 | 121 | *overlay*:: 122 | Toggle the overlay. 123 | 124 | *exec* :: 125 | Execute a shell command. imv provides various environment variables to the 126 | command executed. These are documented in the 'Environment Variables' 127 | section. 128 | 129 | *center*:: 130 | Recenter the selected image. 131 | 132 | *reset*:: 133 | Reset the view, centering the image and using the current scaling mode to 134 | rescale it. 135 | 136 | *next_frame*:: 137 | If an animated gif is currently being displayed, load the next frame. 138 | 139 | *toggle_playing*:: 140 | Toggle playback of the current image if it is an animated gif. 141 | 142 | *scaling* :: 143 | Set the current scaling mode. Setting the mode to 'next' advances it to the 144 | next mode in the list. 145 | 146 | *upscaling* :: 147 | Set the current upscaling method. Setting the method to 'next' advances it to the 148 | next method in the list. 149 | 150 | *slideshow* <+amount|-amount|duration>:: 151 | Increase or decrease the slideshow duration by the given amount in seconds, 152 | or set its duration directly. Aliased to 'ss'. 153 | 154 | *background* :: 155 | Set the background color. 'checks' for a chequerboard pattern, or specify 156 | a 6-digit hexadecimal color code. Aliased to 'bg'. 157 | 158 | *bind* :: 159 | Binds an action to a set of key inputs. Uses the same syntax as the config 160 | file, but without an equals sign between the keys and the commands. For 161 | more information on syntax, see **imv**(5). 162 | 163 | Default Binds 164 | ------------- 165 | 166 | imv comes with several binds configured by default 167 | 168 | *q*:: 169 | Quit 170 | 171 | *Left arrow,*:: 172 | Previous image 173 | 174 | *Right arrow*:: 175 | Next Image 176 | 177 | *gg*:: 178 | Go to first image 179 | 180 | *G*:: 181 | Go to last image 182 | 183 | *j*:: 184 | Pan down 185 | 186 | *k*:: 187 | Pan up 188 | 189 | *h*:: 190 | Pan left 191 | 192 | *l*:: 193 | Pan right 194 | 195 | *x*:: 196 | Close current image 197 | 198 | *f*:: 199 | Toggle fullscreen 200 | 201 | *d*:: 202 | Toggle overlay 203 | 204 | *p*:: 205 | Print current image to stdout 206 | 207 | *Up arrow*:: 208 | Zoom in 209 | 210 | *Down arrow*:: 211 | Zoom out 212 | 213 | *i*:: 214 | Zoom in 215 | 216 | *o*:: 217 | Zoom out 218 | 219 | *+*:: 220 | Zoom in 221 | 222 | *-*:: 223 | Zoom out 224 | 225 | *Ctrl+r*:: 226 | Rotate clockwise by 90 degrees 227 | 228 | *c*:: 229 | Center image 230 | 231 | *s*:: 232 | Next scaling mode 233 | 234 | *S*:: 235 | Next upscaling mode 236 | 237 | *a*:: 238 | Zoom to actual size 239 | 240 | *r*:: 241 | Reset zoom and pan 242 | 243 | *.*:: 244 | Next frame (for animations) 245 | 246 | *Space*:: 247 | Pause/play animations 248 | 249 | *t*:: 250 | Start slideshow/increase delay by 1 second 251 | 252 | *T*:: 253 | Stop slideshow/decrease delay by 1 second 254 | 255 | Configuration 256 | ------------- 257 | 258 | The path to a config file can be given via the *$imv_config* environment 259 | variable. If not found, imv will search for it in the following locations: 260 | 261 | - $XDG_CONFIG_HOME/imv/config (recommended) 262 | - $HOME/.config/imv/config 263 | - $HOME/.imv_config 264 | - $HOME/.imv/config 265 | - /usr/local/etc/imv_config 266 | - /etc/imv_config 267 | 268 | A default config file is shipped with imv into /etc/imv_config 269 | 270 | For documentation on the config file format, see **imv**(5). 271 | 272 | Environment Variables 273 | --------------------- 274 | 275 | When imv executes a shell command, it provides a number of environment variables, 276 | exposing imv's state. These environment variables are also available when 277 | customising the window's title, or the overlay text. 278 | 279 | *$imv_pid*:: 280 | The pid of this instance of imv. Useful for running imv-msg in scripts. 281 | 282 | *$imv_current_file*:: 283 | Path of currently selected image. 284 | 285 | *$imv_scaling_mode*:: 286 | Name of the current scaling mode. 287 | 288 | *$imv_loading*:: 289 | 1 if a new image is loading, 0 otherwise. 290 | 291 | *$imv_current_index*:: 292 | Index of current image, from 1-N. 293 | 294 | *$imv_file_count*:: 295 | Total number of files. 296 | 297 | *$imv_width*:: 298 | Width of the current image. 299 | 300 | *$imv_height*:: 301 | Height of the current image. 302 | 303 | *$imv_scale*:: 304 | Scaling of current image in percent. 305 | 306 | *$imv_slideshow_duration*:: 307 | Number of seconds each image is shown for. 308 | 309 | *$imv_slideshow_elapsed*:: 310 | How long the current image has been shown for. 311 | 312 | IPC 313 | --- 314 | 315 | imv can accept commands from another process over a unix socket. Each instance 316 | of imv will open a unix socket named '$XDG_RUNTIME_DIR/imv-$PID.sock'. If 317 | $XDG_RUNTIME_DIR is undefined, the socket is placed into '/tmp/' instead. 318 | 319 | The **imv-msg**(1) utility is provided to simplify this from shell scripts. 320 | 321 | Authors 322 | ------- 323 | 324 | imv is written and maintained by Harry Jeffery 325 | with contributions from other developers. 326 | 327 | Full source code and other information can be found at 328 | . 329 | 330 | See Also 331 | -------- 332 | 333 | **imv**(5) **imv-msg**(1) **imv-dir**(1) 334 | -------------------------------------------------------------------------------- /src/binds.c: -------------------------------------------------------------------------------- 1 | #include "binds.h" 2 | #include "list.h" 3 | 4 | #include 5 | #include 6 | 7 | struct bind_node { 8 | char *key; /* input key to reach this node */ 9 | struct list *commands; /* commands to run for this node, or NULL if not leaf node */ 10 | struct list *suffixes; /* list of bind_node* suffixes, or NULL if leaf node */ 11 | }; 12 | 13 | struct imv_binds { 14 | struct bind_node bind_tree; 15 | struct list *keys; 16 | bool aborting_sequence; 17 | }; 18 | 19 | static int compare_node_key(const void* item, const void *key) 20 | { 21 | const struct bind_node *node = item; 22 | const char *keystr = key; 23 | return strcmp(node->key, keystr); 24 | } 25 | 26 | static void init_bind_node(struct bind_node *bn) 27 | { 28 | bn->key = NULL; 29 | bn->commands = NULL; 30 | bn->suffixes = list_create(); 31 | } 32 | 33 | static void destroy_bind_node(struct bind_node *bn) 34 | { 35 | for(size_t i = 0; i < bn->suffixes->len; ++i) { 36 | destroy_bind_node(bn->suffixes->items[i]); 37 | } 38 | free(bn->key); 39 | if(bn->commands) { 40 | list_deep_free(bn->commands); 41 | } 42 | list_deep_free(bn->suffixes); 43 | } 44 | 45 | struct imv_binds *imv_binds_create(void) 46 | { 47 | struct imv_binds *binds = calloc(1, sizeof *binds); 48 | init_bind_node(&binds->bind_tree); 49 | binds->keys = list_create(); 50 | return binds; 51 | } 52 | 53 | void imv_binds_free(struct imv_binds *binds) 54 | { 55 | destroy_bind_node(&binds->bind_tree); 56 | list_deep_free(binds->keys); 57 | free(binds); 58 | } 59 | 60 | enum bind_result imv_binds_add(struct imv_binds *binds, const struct list *keys, const char *command) 61 | { 62 | if(!command) { 63 | return BIND_INVALID_COMMAND; 64 | } 65 | 66 | if(!keys) { 67 | return BIND_INVALID_KEYS; 68 | } 69 | 70 | /* Prepare our return code */ 71 | int result = BIND_SUCCESS; 72 | 73 | /* Traverse the trie */ 74 | struct bind_node *node = &binds->bind_tree; 75 | for(size_t i = 0; i < keys->len; ++i) { 76 | /* If we've reached a node that already has a command, there's a conflict */ 77 | if(node->commands) { 78 | result = BIND_INVALID_COMMAND; 79 | break; 80 | } 81 | 82 | /* Find / create a child with the current key */ 83 | struct bind_node *next_node = NULL; 84 | int child_index = list_find(node->suffixes, &compare_node_key, keys->items[i]); 85 | if(child_index == -1) { 86 | /* Create our new node */ 87 | next_node = malloc(sizeof *next_node); 88 | init_bind_node(next_node); 89 | next_node->key = strdup(keys->items[i]); 90 | list_append(node->suffixes, next_node); 91 | } else { 92 | next_node = node->suffixes->items[child_index]; 93 | } 94 | 95 | /* We've now found the correct node for this key */ 96 | 97 | /* Check if the node has a command */ 98 | if(next_node->commands) { 99 | if(i + 1 < keys->len) { 100 | /* If we're not at the end, it's a conflict */ 101 | result = BIND_CONFLICTS; 102 | break; 103 | } else { 104 | /* Otherwise we just need to append a new command to the existing bind. */ 105 | list_append(next_node->commands, strdup(command)); 106 | result = BIND_SUCCESS; 107 | break; 108 | } 109 | } 110 | 111 | if(i + 1 == keys->len) { 112 | /* this is the last key part, try to insert command */ 113 | /* but first, make sure there's no children */ 114 | if(next_node->suffixes->len > 0) { 115 | result = BIND_CONFLICTS; 116 | } else { 117 | next_node->commands = list_create(); 118 | list_append(next_node->commands, strdup(command)); 119 | } 120 | } else { 121 | /* Otherwise, move down the trie */ 122 | node = next_node; 123 | } 124 | } 125 | 126 | return result; 127 | } 128 | 129 | void imv_binds_clear(struct imv_binds *binds) 130 | { 131 | destroy_bind_node(&binds->bind_tree); 132 | init_bind_node(&binds->bind_tree); 133 | } 134 | 135 | void imv_binds_clear_key(struct imv_binds *binds, const struct list *keys) 136 | { 137 | struct bind_node *node = &binds->bind_tree; 138 | 139 | for(size_t i = 0; i < keys->len; ++i) { 140 | /* Traverse the trie to find the right node for the input keys */ 141 | int child_index = list_find(node->suffixes, &compare_node_key, keys->items[i]); 142 | if(child_index == -1) { 143 | /* No such node, no more work to do */ 144 | return; 145 | } else { 146 | node = node->suffixes->items[child_index]; 147 | } 148 | } 149 | 150 | /* We've now found the correct node for the input */ 151 | 152 | /* Clear the commands for this node */ 153 | if(node->commands) { 154 | list_deep_free(node->commands); 155 | node->commands = NULL; 156 | } 157 | } 158 | 159 | enum lookup_result { 160 | LOOKUP_PARTIAL, 161 | LOOKUP_INVALID, 162 | LOOKUP_MATCH, 163 | }; 164 | 165 | static enum lookup_result bind_lookup(struct bind_node *node, struct list *keys, struct list **out_list) 166 | { 167 | for(size_t part = 0; part < keys->len; ++part) { 168 | const char* cur_key = keys->items[part]; 169 | int found = 0; 170 | for(size_t i = 0; i < node->suffixes->len; ++i) { 171 | struct bind_node* cur_node = node->suffixes->items[i]; 172 | if(strcmp(cur_node->key, cur_key) == 0) { 173 | node = node->suffixes->items[i]; 174 | found = 1; 175 | break; 176 | } 177 | } 178 | if(!found) { 179 | return LOOKUP_INVALID; 180 | } 181 | } 182 | 183 | if(node->commands) { 184 | *out_list = node->commands; 185 | return LOOKUP_MATCH; 186 | } 187 | return LOOKUP_PARTIAL; 188 | } 189 | 190 | void imv_bind_clear_input(struct imv_binds *binds) 191 | { 192 | list_deep_free(binds->keys); 193 | binds->keys = list_create(); 194 | } 195 | 196 | struct list *imv_bind_handle_event(struct imv_binds *binds, const char *event) 197 | { 198 | /* If the user hits Escape twice in a row, treat that as backtracking out 199 | * of the current key sequence. */ 200 | if (!strcmp("Escape", event)) { 201 | if (binds->aborting_sequence) { 202 | /* The last thing they hit was escape, so abort the current entry */ 203 | binds->aborting_sequence = false; 204 | imv_bind_clear_input(binds); 205 | return NULL; 206 | } else { 207 | /* Start the aborting sequence */ 208 | binds->aborting_sequence = true; 209 | } 210 | } else { 211 | /* They didn't hit escape, if they were in an abort sequence, cancel it */ 212 | binds->aborting_sequence = false; 213 | } 214 | 215 | list_append(binds->keys, strdup(event)); 216 | 217 | struct list *commands = NULL; 218 | enum lookup_result result = bind_lookup(&binds->bind_tree, binds->keys, &commands); 219 | if(result == LOOKUP_PARTIAL) { 220 | return NULL; 221 | } else if(result == LOOKUP_MATCH) { 222 | imv_bind_clear_input(binds); 223 | return commands; 224 | } else if(result == LOOKUP_INVALID) { 225 | imv_bind_clear_input(binds); 226 | return NULL; 227 | } 228 | 229 | /* Should not happen */ 230 | imv_bind_clear_input(binds); 231 | return NULL; 232 | } 233 | 234 | struct list *imv_bind_parse_keys(const char *keys) 235 | { 236 | struct list *list = list_create(); 237 | 238 | /* Iterate through the string, breaking it into its parts */ 239 | while(*keys) { 240 | 241 | if(*keys == '<') { 242 | /* Keyname block, need to extract the name, and convert it */ 243 | const char *end = keys; 244 | while(*end && *end != '>') { 245 | ++end; 246 | } 247 | if(*end == '>') { 248 | /* We've got a block. Check if it's a valid special key name. */ 249 | const size_t key_len = end - keys; 250 | char *key = malloc(key_len); 251 | memcpy(key, keys + 1 /* skip the '<' */, key_len - 1); 252 | key[key_len - 1] = 0; 253 | list_append(list, key); 254 | keys = end + 1; 255 | } else { 256 | /* A block that didn't have a closing '<'. Abort. */ 257 | list_deep_free(list); 258 | return NULL; 259 | } 260 | } else { 261 | /* Just a regular character */ 262 | char *item = malloc(2); 263 | item[0] = *keys; 264 | item[1] = 0; 265 | list_append(list, item); 266 | ++keys; 267 | } 268 | } 269 | 270 | return list; 271 | } 272 | 273 | size_t imv_bind_print_keylist(const struct list *keys, char *buf, size_t len) 274 | { 275 | size_t printed = 0; 276 | 277 | /* iterate through all the keys, wrapping them in '<' and '>' if needed */ 278 | for(size_t i = 0; i < keys->len; ++i) { 279 | const char *key = keys->items[i]; 280 | const char *format = strlen(key) > 1 ? "<%s>" : "%s"; 281 | printed += snprintf(buf, len - printed, format, key); 282 | } 283 | return printed; 284 | } 285 | -------------------------------------------------------------------------------- /src/navigator.c: -------------------------------------------------------------------------------- 1 | #include "navigator.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "list.h" 15 | 16 | /* Some systems like GNU/Hurd don't define PATH_MAX */ 17 | #ifndef PATH_MAX 18 | #define PATH_MAX 4096 19 | #endif 20 | 21 | struct nav_item { 22 | char *path; 23 | }; 24 | 25 | struct imv_navigator { 26 | struct list *paths; 27 | size_t cur_path; 28 | time_t last_change; 29 | time_t last_check; 30 | int last_move_direction; 31 | int changed; 32 | int wrapped; 33 | }; 34 | 35 | struct imv_navigator *imv_navigator_create(void) 36 | { 37 | struct imv_navigator *nav = calloc(1, sizeof *nav); 38 | nav->last_move_direction = 1; 39 | nav->paths = list_create(); 40 | return nav; 41 | } 42 | 43 | void imv_navigator_free(struct imv_navigator *nav) 44 | { 45 | for (size_t i = 0; i < nav->paths->len; ++i) { 46 | struct nav_item *nav_item = nav->paths->items[i]; 47 | free(nav_item->path); 48 | } 49 | list_deep_free(nav->paths); 50 | free(nav); 51 | } 52 | 53 | static int add_item(struct imv_navigator *nav, const char *path) 54 | { 55 | struct nav_item *nav_item = calloc(1, sizeof *nav_item); 56 | 57 | nav_item->path = realpath(path, NULL); 58 | if (!nav_item->path) { 59 | nav_item->path = strdup(path); 60 | } 61 | 62 | list_append(nav->paths, nav_item); 63 | 64 | if (nav->paths->len == 1) { 65 | nav->cur_path = 0; 66 | nav->changed = 1; 67 | } 68 | 69 | return 0; 70 | } 71 | 72 | int imv_navigator_add(struct imv_navigator *nav, const char *path, 73 | int recursive) 74 | { 75 | char path_buf[PATH_MAX+1]; 76 | struct stat path_info; 77 | if ((stat(path, &path_info) == 0) && 78 | S_ISDIR(path_info.st_mode)) { 79 | int result = 0; 80 | struct dirent **dir_list; 81 | int total_dirs = scandir(path, &dir_list, 0, alphasort); 82 | if (total_dirs >= 0) { 83 | for (int i = 0; i < total_dirs; ++i) { 84 | struct dirent *dir = dir_list[i]; 85 | if (strcmp(dir->d_name, "..") == 0 || strcmp(dir->d_name, ".") == 0) { 86 | continue; 87 | } 88 | snprintf(path_buf, sizeof path_buf, "%s/%s", path, dir->d_name); 89 | struct stat new_path_info; 90 | if (stat(path_buf, &new_path_info)) { 91 | switch (errno) { 92 | case ELOOP: 93 | case ENOTDIR: 94 | case ENOENT: continue; 95 | default: result = 1; break; 96 | } 97 | } 98 | int is_dir = S_ISDIR(new_path_info.st_mode); 99 | if (is_dir && recursive) { 100 | if (imv_navigator_add(nav, path_buf, recursive) != 0) { 101 | result = 1; 102 | break; 103 | } 104 | } else if (!is_dir) { 105 | if (add_item(nav, path_buf) != 0) { 106 | result = 1; 107 | break; 108 | } 109 | } 110 | free(dir_list[i]); 111 | } 112 | free(dir_list); 113 | } 114 | return result; 115 | } else { 116 | return add_item(nav, path); 117 | } 118 | 119 | return 0; 120 | } 121 | 122 | const char *imv_navigator_selection(struct imv_navigator *nav) 123 | { 124 | const char *path = imv_navigator_at(nav, nav->cur_path); 125 | return path ? path : ""; 126 | } 127 | 128 | size_t imv_navigator_index(struct imv_navigator *nav) 129 | { 130 | return nav->cur_path; 131 | } 132 | 133 | void imv_navigator_select_rel(struct imv_navigator *nav, ssize_t direction) 134 | { 135 | const ssize_t prev_path = nav->cur_path; 136 | if (nav->paths->len == 0) { 137 | return; 138 | } 139 | 140 | if (direction > 1) { 141 | direction = div(direction, nav->paths->len).rem; 142 | } else if (direction < -1) { 143 | direction = div(direction, nav->paths->len).rem; 144 | } else if (direction == 0) { 145 | return; 146 | } 147 | 148 | ssize_t new_path = nav->cur_path + direction; 149 | if (new_path >= (ssize_t)nav->paths->len) { 150 | /* Wrap after the end of the list */ 151 | new_path -= (ssize_t)nav->paths->len; 152 | nav->wrapped = 1; 153 | } else if (new_path < 0) { 154 | /* Wrap before the start of the list */ 155 | new_path += (ssize_t)nav->paths->len; 156 | nav->wrapped = 1; 157 | } 158 | nav->cur_path = (size_t)new_path; 159 | nav->last_move_direction = direction; 160 | nav->changed = prev_path != new_path; 161 | return; 162 | } 163 | 164 | void imv_navigator_select_abs(struct imv_navigator *nav, ssize_t index) 165 | { 166 | /* allow -1 to indicate the last image */ 167 | if (index < 0) { 168 | index += (ssize_t)nav->paths->len; 169 | 170 | /* but if they go farther back than the first image, stick to first image */ 171 | if (index < 0) { 172 | index = 0; 173 | } 174 | } 175 | 176 | /* stick to last image if we go beyond it */ 177 | if (index >= (ssize_t)nav->paths->len) { 178 | index = (ssize_t)nav->paths->len - 1; 179 | } 180 | 181 | const size_t prev_path = nav->cur_path; 182 | nav->cur_path = (size_t)index; 183 | nav->changed = prev_path != nav->cur_path; 184 | nav->last_move_direction = (index >= (ssize_t)prev_path) ? 1 : -1; 185 | } 186 | 187 | void imv_navigator_remove(struct imv_navigator *nav, const char *path) 188 | { 189 | bool found = false; 190 | size_t removed = 0; 191 | for (size_t i = 0; i < nav->paths->len; ++i) { 192 | struct nav_item *item = nav->paths->items[i]; 193 | if (!strcmp(item->path, path)) { 194 | free(item->path); 195 | free(item); 196 | list_remove(nav->paths, i); 197 | removed = i; 198 | found = true; 199 | break; 200 | } 201 | } 202 | 203 | if (!found) { 204 | return; 205 | } 206 | 207 | if (nav->cur_path == removed) { 208 | /* We just removed the current path */ 209 | if (nav->last_move_direction < 0) { 210 | /* Move left */ 211 | imv_navigator_select_rel(nav, -1); 212 | } else { 213 | /* Try to stay where we are, unless we ran out of room */ 214 | if (nav->cur_path == nav->paths->len) { 215 | nav->cur_path = 0; 216 | nav->wrapped = 1; 217 | } 218 | } 219 | } 220 | nav->changed = 1; 221 | } 222 | 223 | void imv_navigator_remove_at(struct imv_navigator *nav, size_t index) 224 | { 225 | if (index >= nav->paths->len) { 226 | return; 227 | } 228 | struct nav_item *item = nav->paths->items[index]; 229 | free(item->path); 230 | free(item); 231 | list_remove(nav->paths, index); 232 | 233 | if (nav->cur_path == index) { 234 | /* We just removed the current path */ 235 | if (nav->last_move_direction < 0) { 236 | /* Move left */ 237 | imv_navigator_select_rel(nav, -1); 238 | } else { 239 | /* Try to stay where we are, unless we ran out of room */ 240 | if (nav->cur_path == nav->paths->len) { 241 | nav->cur_path = 0; 242 | nav->wrapped = 1; 243 | } 244 | } 245 | } 246 | nav->changed = 1; 247 | } 248 | 249 | void imv_navigator_remove_all(struct imv_navigator *nav) 250 | { 251 | for (size_t i = 0; i < nav->paths->len; ++i) { 252 | struct nav_item *item = nav->paths->items[i]; 253 | free(item->path); 254 | free(item); 255 | } 256 | list_clear(nav->paths); 257 | nav->cur_path = 0; 258 | nav->changed = 1; 259 | } 260 | 261 | ssize_t imv_navigator_find_path(struct imv_navigator *nav, const char *path) 262 | { 263 | char *real_path = realpath(path, NULL); 264 | if (real_path) { 265 | /* first try to match the exact path if path can be resolved */ 266 | for (size_t i = 0; i < nav->paths->len; ++i) { 267 | struct nav_item *item = nav->paths->items[i]; 268 | if (!strcmp(item->path, real_path)) { 269 | free(real_path); 270 | return (ssize_t)i; 271 | } 272 | } 273 | 274 | free(real_path); 275 | } 276 | 277 | /* no exact matches or path cannot be resolved, try the final portion of the path */ 278 | for (size_t i = 0; i < nav->paths->len; ++i) { 279 | struct nav_item *item = nav->paths->items[i]; 280 | char *last_sep = strrchr(item->path, '/'); 281 | if (last_sep && !strcmp(last_sep+1, path)) { 282 | return (ssize_t)i; 283 | } 284 | } 285 | 286 | /* no matches at all, give up */ 287 | return -1; 288 | } 289 | 290 | int imv_navigator_poll_changed(struct imv_navigator *nav) 291 | { 292 | if (nav->changed) { 293 | nav->changed = 0; 294 | nav->last_change = time(NULL); 295 | return 1; 296 | } 297 | 298 | if (nav->paths->len == 0) { 299 | return 0; 300 | }; 301 | 302 | time_t cur_time = time(NULL); 303 | /* limit polling to once per second */ 304 | if (nav->last_check < cur_time - 1) { 305 | nav->last_check = cur_time; 306 | 307 | struct stat file_info; 308 | struct nav_item *cur_item = nav->paths->items[nav->cur_path]; 309 | if (stat(cur_item->path, &file_info) == -1) { 310 | return 0; 311 | } 312 | 313 | time_t file_changed = file_info.st_mtim.tv_sec; 314 | if (file_changed > nav->last_change) { 315 | nav->last_change = file_changed; 316 | return 1; 317 | } 318 | } 319 | return 0; 320 | } 321 | 322 | int imv_navigator_wrapped(struct imv_navigator *nav) 323 | { 324 | return nav->wrapped; 325 | } 326 | 327 | size_t imv_navigator_length(struct imv_navigator *nav) 328 | { 329 | return nav->paths->len; 330 | } 331 | 332 | char *imv_navigator_at(struct imv_navigator *nav, size_t index) 333 | { 334 | if (index < nav->paths->len) { 335 | struct nav_item *item = nav->paths->items[index]; 336 | return item->path; 337 | } 338 | return NULL; 339 | } 340 | 341 | /* vim:set ts=2 sts=2 sw=2 et: */ 342 | -------------------------------------------------------------------------------- /src/backend_freeimage.c: -------------------------------------------------------------------------------- 1 | #include "backend.h" 2 | #include "bitmap.h" 3 | #include "image.h" 4 | #include "log.h" 5 | #include "source.h" 6 | #include "source_private.h" 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | struct private { 13 | char *path; 14 | FIMEMORY *memory; 15 | FREE_IMAGE_FORMAT format; 16 | FIMULTIBITMAP *multibitmap; 17 | FIBITMAP *last_frame; 18 | int num_frames; 19 | int next_frame; 20 | int width; 21 | int height; 22 | }; 23 | 24 | static void free_private(void *raw_private) 25 | { 26 | if (!raw_private) { 27 | return; 28 | } 29 | 30 | struct private *private = raw_private; 31 | 32 | free(private->path); 33 | private->path = NULL; 34 | 35 | if (private->memory) { 36 | FreeImage_CloseMemory(private->memory); 37 | private->memory = NULL; 38 | } 39 | 40 | if (private->multibitmap) { 41 | FreeImage_CloseMultiBitmap(private->multibitmap, 0); 42 | private->multibitmap = NULL; 43 | } 44 | 45 | if (private->last_frame) { 46 | FreeImage_Unload(private->last_frame); 47 | private->last_frame = NULL; 48 | } 49 | 50 | free(private); 51 | } 52 | 53 | static struct imv_image *to_image(FIBITMAP *in_bmp) 54 | { 55 | struct imv_bitmap *bmp = malloc(sizeof *bmp); 56 | bmp->width = FreeImage_GetWidth(in_bmp); 57 | bmp->height = FreeImage_GetHeight(in_bmp); 58 | bmp->format = IMV_ARGB; 59 | bmp->data = malloc(4 * bmp->width * bmp->height); 60 | FreeImage_ConvertToRawBits(bmp->data, in_bmp, 4 * bmp->width, 32, 61 | FI_RGBA_RED_MASK, FI_RGBA_GREEN_MASK, FI_RGBA_BLUE_MASK, TRUE); 62 | struct imv_image *image = imv_image_create_from_bitmap(bmp); 63 | return image; 64 | } 65 | 66 | static FIBITMAP *normalise_bitmap(FIBITMAP *input) 67 | { 68 | FIBITMAP *output = NULL; 69 | 70 | switch (FreeImage_GetImageType(input)) { 71 | case FIT_RGB16: 72 | case FIT_RGBA16: 73 | case FIT_RGBF: 74 | case FIT_RGBAF: 75 | output = FreeImage_ConvertTo32Bits(input); 76 | FreeImage_Unload(input); 77 | break; 78 | 79 | case FIT_UINT16: 80 | case FIT_INT16: 81 | case FIT_UINT32: 82 | case FIT_INT32: 83 | case FIT_FLOAT: 84 | case FIT_DOUBLE: 85 | case FIT_COMPLEX: 86 | output = FreeImage_ConvertTo8Bits(input); 87 | FreeImage_Unload(input); 88 | break; 89 | 90 | case FIT_BITMAP: 91 | default: 92 | output = input; 93 | } 94 | 95 | imv_log(IMV_DEBUG, 96 | "freeimage: bitmap normalised to 32 bits: before=%p after=%p\n", 97 | input, output); 98 | return output; 99 | } 100 | 101 | static void first_frame(void *raw_private, struct imv_image **image, int *frametime) 102 | { 103 | *image = NULL; 104 | *frametime = 0; 105 | 106 | imv_log(IMV_DEBUG, "freeimage: first_frame called\n"); 107 | 108 | FIBITMAP *bmp = NULL; 109 | 110 | struct private *private = raw_private; 111 | 112 | if (private->format == FIF_GIF) { 113 | if (private->path) { 114 | private->multibitmap = FreeImage_OpenMultiBitmap(FIF_GIF, private->path, 115 | /* don't create file */ 0, 116 | /* read only */ 1, 117 | /* keep in memory */ 1, 118 | /* flags */ GIF_LOAD256); 119 | } else if (private->memory) { 120 | private->multibitmap = FreeImage_LoadMultiBitmapFromMemory(FIF_GIF, 121 | private->memory, 122 | /* flags */ GIF_LOAD256); 123 | } else { 124 | imv_log(IMV_ERROR, "private->path and private->memory both NULL"); 125 | return; 126 | } 127 | 128 | if (!private->multibitmap) { 129 | imv_log(IMV_ERROR, "first frame already loaded"); 130 | return; 131 | } 132 | 133 | FIBITMAP *frame = FreeImage_LockPage(private->multibitmap, 0); 134 | 135 | private->num_frames = FreeImage_GetPageCount(private->multibitmap); 136 | 137 | /* Get duration of first frame */ 138 | FITAG *tag = NULL; 139 | FreeImage_GetMetadata(FIMD_ANIMATION, frame, "FrameTime", &tag); 140 | if (FreeImage_GetTagValue(tag)) { 141 | *frametime = *(int*)FreeImage_GetTagValue(tag); 142 | } else { 143 | *frametime = 100; /* default value for gifs */ 144 | } 145 | bmp = FreeImage_ConvertTo24Bits(frame); 146 | FreeImage_UnlockPage(private->multibitmap, frame, 0); 147 | 148 | } else { /* not a gif */ 149 | private->num_frames = 1; 150 | int flags = (private->format == FIF_JPEG) ? JPEG_EXIFROTATE : 0; 151 | FIBITMAP *fibitmap = NULL; 152 | if (private->path) { 153 | fibitmap = FreeImage_Load(private->format, private->path, flags); 154 | } else if (private->memory) { 155 | fibitmap = FreeImage_LoadFromMemory(private->format, private->memory, flags); 156 | } 157 | if (!fibitmap) { 158 | imv_log(IMV_ERROR, "FreeImage_Load returned NULL"); 159 | return; 160 | } 161 | 162 | bmp = normalise_bitmap(fibitmap); 163 | } 164 | 165 | private->width = FreeImage_GetWidth(bmp); 166 | private->height = FreeImage_GetHeight(bmp); 167 | private->last_frame = bmp; 168 | private->next_frame = 1 % private->num_frames; 169 | 170 | *image = to_image(bmp); 171 | } 172 | 173 | static void next_frame(void *raw_private, struct imv_image **image, int *frametime) 174 | { 175 | *image = NULL; 176 | *frametime = 0; 177 | 178 | struct private *private = raw_private; 179 | 180 | if (private->num_frames == 1) { 181 | *image = to_image(private->last_frame); 182 | return; 183 | } 184 | 185 | FITAG *tag = NULL; 186 | char disposal_method = 0; 187 | short top = 0; 188 | short left = 0; 189 | 190 | FIBITMAP *frame = FreeImage_LockPage(private->multibitmap, private->next_frame); 191 | FIBITMAP *frame32 = FreeImage_ConvertTo32Bits(frame); 192 | 193 | /* First frame is always going to use the raw frame */ 194 | if (private->next_frame > 0) { 195 | FreeImage_GetMetadata(FIMD_ANIMATION, frame, "DisposalMethod", &tag); 196 | if (FreeImage_GetTagValue(tag)) { 197 | disposal_method = *(char*)FreeImage_GetTagValue(tag); 198 | } 199 | } 200 | 201 | FreeImage_GetMetadata(FIMD_ANIMATION, frame, "FrameLeft", &tag); 202 | if (FreeImage_GetTagValue(tag)) { 203 | left = *(short*)FreeImage_GetTagValue(tag); 204 | } 205 | 206 | FreeImage_GetMetadata(FIMD_ANIMATION, frame, "FrameTop", &tag); 207 | if (FreeImage_GetTagValue(tag)) { 208 | top = *(short*)FreeImage_GetTagValue(tag); 209 | } 210 | 211 | FreeImage_GetMetadata(FIMD_ANIMATION, frame, "FrameTime", &tag); 212 | if (FreeImage_GetTagValue(tag)) { 213 | *frametime = *(int*)FreeImage_GetTagValue(tag); 214 | } 215 | 216 | /* some gifs don't provide a frame time at all */ 217 | if (*frametime == 0) { 218 | *frametime = 100; 219 | } 220 | 221 | FreeImage_UnlockPage(private->multibitmap, frame, 0); 222 | 223 | /* If this frame is inset, we need to expand it for compositing */ 224 | if (private->width != (int)FreeImage_GetWidth(frame32) || 225 | private->height != (int)FreeImage_GetHeight(frame32)) { 226 | FIBITMAP *expanded = FreeImage_Allocate(private->width, private->height, 32, 0,0,0); 227 | FreeImage_Paste(expanded, frame32, left, top, 255); 228 | FreeImage_Unload(frame32); 229 | frame32 = expanded; 230 | } 231 | 232 | switch(disposal_method) { 233 | case 0: /* nothing specified, fall through to compositing */ 234 | case 1: /* composite over previous frame */ 235 | if (private->last_frame && private->next_frame > 0) { 236 | FIBITMAP *bg_frame = FreeImage_ConvertTo24Bits(private->last_frame); 237 | FreeImage_Unload(private->last_frame); 238 | FIBITMAP *comp = FreeImage_Composite(frame32, 1, NULL, bg_frame); 239 | FreeImage_Unload(bg_frame); 240 | FreeImage_Unload(frame32); 241 | private->last_frame = comp; 242 | } else { 243 | /* No previous frame, just render directly */ 244 | if(private->last_frame) { 245 | FreeImage_Unload(private->last_frame); 246 | } 247 | private->last_frame = frame32; 248 | } 249 | break; 250 | case 2: /* TODO - set to background, composite over that */ 251 | if (private->last_frame) { 252 | FreeImage_Unload(private->last_frame); 253 | } 254 | private->last_frame = frame32; 255 | break; 256 | case 3: /* TODO - restore to previous content */ 257 | if (private->last_frame) { 258 | FreeImage_Unload(private->last_frame); 259 | } 260 | private->last_frame = frame32; 261 | break; 262 | } 263 | 264 | private->next_frame = (private->next_frame + 1) % private->num_frames; 265 | 266 | *image = to_image(private->last_frame); 267 | } 268 | 269 | static const struct imv_source_vtable vtable = { 270 | .load_first_frame = first_frame, 271 | .load_next_frame = next_frame, 272 | .free = free_private 273 | }; 274 | 275 | static enum backend_result open_path(const char *path, struct imv_source **src) 276 | { 277 | imv_log(IMV_DEBUG, "freeimage: open_path(%s)\n", path); 278 | FREE_IMAGE_FORMAT fmt = FreeImage_GetFileType(path, 0); 279 | 280 | if (fmt == FIF_UNKNOWN) { 281 | imv_log(IMV_DEBUG, "freeimage: unknown file format\n"); 282 | return BACKEND_UNSUPPORTED; 283 | } 284 | 285 | struct private *private = calloc(1, sizeof(struct private)); 286 | private->format = fmt; 287 | private->path = strdup(path); 288 | 289 | *src = imv_source_create(&vtable, private); 290 | return BACKEND_SUCCESS; 291 | } 292 | 293 | static enum backend_result open_memory(void *data, size_t len, struct imv_source **src) 294 | { 295 | FIMEMORY *fmem = FreeImage_OpenMemory(data, len); 296 | 297 | FREE_IMAGE_FORMAT fmt = FreeImage_GetFileTypeFromMemory(fmem, 0); 298 | 299 | if (fmt == FIF_UNKNOWN) { 300 | FreeImage_CloseMemory(fmem); 301 | return BACKEND_UNSUPPORTED; 302 | } 303 | 304 | struct private *private = calloc(1, sizeof(struct private)); 305 | private->format = fmt; 306 | private->memory = fmem; 307 | private->path = NULL; 308 | 309 | *src = imv_source_create(&vtable, private); 310 | return BACKEND_SUCCESS; 311 | } 312 | 313 | const struct imv_backend imv_backend_freeimage = { 314 | .name = "FreeImage", 315 | .description = "Open source image library supporting a large number of formats", 316 | .website = "http://freeimage.sourceforge.net/", 317 | .license = "FreeImage Public License v1.0", 318 | .open_path = &open_path, 319 | .open_memory = &open_memory, 320 | }; 321 | -------------------------------------------------------------------------------- /src/viewport.c: -------------------------------------------------------------------------------- 1 | #include "viewport.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | struct imv_viewport { 8 | double scale; 9 | double rotation; 10 | bool mirrored; 11 | struct { 12 | int width, height; 13 | } window; /* window dimensions */ 14 | struct { 15 | int width, height; 16 | } buffer; /* rendering buffer dimensions */ 17 | int x, y; 18 | double pan_factor_x, pan_factor_y; 19 | int redraw; 20 | int playing; 21 | int locked; 22 | }; 23 | 24 | static void input_xy_to_render_xy(struct imv_viewport *view, int *x, int *y) 25 | { 26 | *x *= view->buffer.width / view->window.width; 27 | *y *= view->buffer.height / view->window.height; 28 | } 29 | 30 | struct imv_viewport *imv_viewport_create(int window_width, int window_height, 31 | int buffer_width, int buffer_height) 32 | { 33 | struct imv_viewport *view = malloc(sizeof *view); 34 | view->window.width = window_width; 35 | view->window.height = window_height; 36 | view->buffer.width = buffer_width; 37 | view->buffer.height = buffer_height; 38 | view->scale = 1; 39 | view->rotation = 0; 40 | view->mirrored = false; 41 | view->x = view->y = view->redraw = 0; 42 | view->pan_factor_x = view->pan_factor_y = 0.5; 43 | view->playing = 1; 44 | view->locked = 0; 45 | return view; 46 | } 47 | 48 | void imv_viewport_free(struct imv_viewport *view) 49 | { 50 | free(view); 51 | } 52 | 53 | void imv_viewport_set_playing(struct imv_viewport *view, bool playing) 54 | { 55 | view->playing = playing; 56 | } 57 | 58 | bool imv_viewport_is_playing(struct imv_viewport *view) 59 | { 60 | return view->playing; 61 | } 62 | 63 | void imv_viewport_toggle_playing(struct imv_viewport *view) 64 | { 65 | view->playing = !view->playing; 66 | } 67 | 68 | void imv_viewport_scale_to_actual(struct imv_viewport *view, const struct imv_image *image) 69 | { 70 | view->scale = 1; 71 | view->redraw = 1; 72 | view->locked = 1; 73 | imv_viewport_center(view, image); 74 | } 75 | 76 | void imv_viewport_get_offset(struct imv_viewport *view, int *x, int *y) 77 | { 78 | if(x) { 79 | *x = view->x; 80 | } 81 | if(y) { 82 | *y = view->y; 83 | } 84 | } 85 | 86 | void imv_viewport_get_scale(struct imv_viewport *view, double *scale) 87 | { 88 | if(scale) { 89 | *scale = view->scale; 90 | } 91 | } 92 | 93 | void imv_viewport_get_rotation(struct imv_viewport *view, double *rotation) 94 | { 95 | if(rotation) { 96 | *rotation = view->rotation; 97 | } 98 | } 99 | 100 | void imv_viewport_get_mirrored(struct imv_viewport *view, bool *mirrored) 101 | { 102 | if(mirrored) { 103 | *mirrored = view->mirrored; 104 | } 105 | } 106 | 107 | void imv_viewport_set_default_pan_factor(struct imv_viewport *view, double pan_factor_x, double pan_factor_y) 108 | { 109 | view->pan_factor_x = pan_factor_x; 110 | view->pan_factor_y = pan_factor_y; 111 | } 112 | 113 | void imv_viewport_move(struct imv_viewport *view, int x, int y, 114 | const struct imv_image *image) 115 | { 116 | input_xy_to_render_xy(view, &x, &y); 117 | view->x += x; 118 | view->y += y; 119 | view->redraw = 1; 120 | view->locked = 1; 121 | int w = (int)(imv_image_width(image) * view->scale); 122 | int h = (int)(imv_image_height(image) * view->scale); 123 | if (view->x < -w) { 124 | view->x = -w; 125 | } 126 | if (view->x > view->buffer.width) { 127 | view->x = view->buffer.width; 128 | } 129 | if (view->y < -h) { 130 | view->y = -h; 131 | } 132 | if (view->y > view->buffer.height) { 133 | view->y = view->buffer.height; 134 | } 135 | } 136 | 137 | void imv_viewport_zoom(struct imv_viewport *view, const struct imv_image *image, 138 | enum imv_zoom_source src, int mouse_x, int mouse_y, int amount) 139 | { 140 | double prev_scale = view->scale; 141 | int x, y; 142 | 143 | const int image_width = imv_image_width(image); 144 | const int image_height = imv_image_height(image); 145 | 146 | /* x and y coordinates are relative to the image */ 147 | if(src == IMV_ZOOM_MOUSE) { 148 | input_xy_to_render_xy(view, &mouse_x, &mouse_y); 149 | x = mouse_x - view->x; 150 | y = mouse_y - view->y; 151 | } else { 152 | x = view->scale * image_width / 2; 153 | y = view->scale * image_height / 2; 154 | } 155 | 156 | const int scaled_width = image_width * view->scale; 157 | const int scaled_height = image_height * view->scale; 158 | const int ic_x = view->x + scaled_width/2; 159 | const int ic_y = view->y + scaled_height/2; 160 | const int wc_x = view->buffer.width/2; 161 | const int wc_y = view->buffer.height/2; 162 | 163 | const double scale_factor = pow(1.04, amount); 164 | view->scale *= scale_factor; 165 | 166 | const double min_scale = 0.1; 167 | const double max_scale = 100; 168 | if(view->scale > max_scale) { 169 | view->scale = max_scale; 170 | } else if (view->scale < min_scale) { 171 | view->scale = min_scale; 172 | } 173 | 174 | if(view->scale < prev_scale) { 175 | if(scaled_width < view->buffer.width) { 176 | x = scaled_width/2 - (ic_x - wc_x)*2; 177 | } 178 | if(scaled_height < view->buffer.height) { 179 | y = scaled_height/2 - (ic_y - wc_y)*2; 180 | } 181 | } else { 182 | if(scaled_width < view->buffer.width) { 183 | x = scaled_width/2; 184 | } 185 | if(scaled_height < view->buffer.height) { 186 | y = scaled_height/2; 187 | } 188 | } 189 | 190 | const double changeX = x - (x * (view->scale / prev_scale)); 191 | const double changeY = y - (y * (view->scale / prev_scale)); 192 | 193 | view->x += changeX; 194 | view->y += changeY; 195 | 196 | view->redraw = 1; 197 | view->locked = 1; 198 | } 199 | 200 | void imv_viewport_rotate_by(struct imv_viewport *view, double degrees) { 201 | view->rotation += degrees; 202 | } 203 | 204 | void imv_viewport_rotate_to(struct imv_viewport *view, double degrees) { 205 | view->rotation = degrees; 206 | } 207 | 208 | void imv_viewport_flip_h(struct imv_viewport *view) { 209 | view->mirrored = !view->mirrored; 210 | } 211 | 212 | void imv_viewport_flip_v(struct imv_viewport *view) { 213 | view->mirrored = !view->mirrored; 214 | view->rotation = view->rotation - 180; 215 | } 216 | 217 | void imv_viewport_reset_transform(struct imv_viewport *view) { 218 | view->mirrored = false; 219 | view->rotation = 0; 220 | } 221 | 222 | void imv_viewport_center(struct imv_viewport *view, const struct imv_image *image) 223 | { 224 | const int image_width = imv_image_width(image); 225 | const int image_height = imv_image_height(image); 226 | 227 | view->x = view->buffer.width - image_width * view->scale; 228 | view->y = view->buffer.height - image_height * view->scale; 229 | 230 | if (view->x > 0) { 231 | /* Image is smaller than the window. Center the image */ 232 | view->x *= 0.5; 233 | } else { 234 | view->x *= view->pan_factor_x; 235 | } 236 | 237 | if (view->y > 0) { 238 | /* Image is smaller than the window. Center the image */ 239 | view->y *= 0.5; 240 | } else { 241 | view->y *= view->pan_factor_y; 242 | } 243 | 244 | view->locked = 1; 245 | view->redraw = 1; 246 | } 247 | 248 | void imv_viewport_scale_to_window(struct imv_viewport *view, const struct imv_image *image) 249 | { 250 | const int image_width = imv_image_width(image); 251 | const int image_height = imv_image_height(image); 252 | const double window_aspect = (double)view->buffer.width / (double)view->buffer.height; 253 | const double image_aspect = (double)image_width / (double)image_height; 254 | 255 | if(window_aspect > image_aspect) { 256 | /* Image will become too tall before it becomes too wide */ 257 | view->scale = (double)view->buffer.height / (double)image_height; 258 | } else { 259 | /* Image will become too wide before it becomes too tall */ 260 | view->scale = (double)view->buffer.width / (double)image_width; 261 | } 262 | 263 | imv_viewport_center(view, image); 264 | view->locked = 0; 265 | } 266 | 267 | void imv_viewport_crop_to_window(struct imv_viewport *view, const struct imv_image *image) 268 | { 269 | const int image_width = imv_image_width(image); 270 | const int image_height = imv_image_height(image); 271 | const double window_aspect = (double)view->buffer.width / (double)view->buffer.height; 272 | const double image_aspect = (double)image_width / (double)image_height; 273 | 274 | /* Scale the image so that it fills the whole window */ 275 | if(window_aspect > image_aspect) { 276 | view->scale = (double)view->buffer.width / (double)image_width; 277 | } else { 278 | view->scale = (double)view->buffer.height / (double)image_height; 279 | } 280 | 281 | imv_viewport_center(view, image); 282 | view->locked = 0; 283 | } 284 | 285 | void imv_viewport_set_redraw(struct imv_viewport *view) 286 | { 287 | view->redraw = 1; 288 | } 289 | 290 | void imv_viewport_rescale(struct imv_viewport *view, const struct imv_image *image, 291 | enum scaling_mode scaling_mode) { 292 | if (scaling_mode == SCALING_NONE || 293 | (scaling_mode == SCALING_DOWN 294 | && view->buffer.width > imv_image_width(image) 295 | && view->buffer.height > imv_image_height(image))) { 296 | imv_viewport_scale_to_actual(view, image); 297 | } else if (scaling_mode == SCALING_CROP) { 298 | imv_viewport_crop_to_window(view, image); 299 | } else { 300 | imv_viewport_scale_to_window(view, image); 301 | } 302 | } 303 | 304 | void imv_viewport_update(struct imv_viewport *view, 305 | int window_width, int window_height, 306 | int buffer_width, int buffer_height, 307 | struct imv_image *image, 308 | enum scaling_mode scaling_mode) 309 | { 310 | view->window.width = window_width; 311 | view->window.height = window_height; 312 | view->buffer.width = buffer_width; 313 | view->buffer.height = buffer_height; 314 | 315 | view->redraw = 1; 316 | if(view->locked) { 317 | return; 318 | } 319 | 320 | imv_viewport_center(view, image); 321 | imv_viewport_rescale(view, image, scaling_mode); 322 | } 323 | 324 | int imv_viewport_needs_redraw(struct imv_viewport *view) 325 | { 326 | int redraw = 0; 327 | if(view->redraw) { 328 | redraw = 1; 329 | view->redraw = 0; 330 | } 331 | return redraw; 332 | } 333 | 334 | 335 | /* vim:set ts=2 sts=2 sw=2 et: */ 336 | -------------------------------------------------------------------------------- /src/canvas.c: -------------------------------------------------------------------------------- 1 | #include "canvas.h" 2 | 3 | #include "image.h" 4 | #include "log.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #ifdef IMV_BACKEND_LIBRSVG 17 | #include 18 | #endif 19 | 20 | // 16x16 chequerboard texture data 21 | #define REPEAT8(...) __VA_ARGS__, __VA_ARGS__, __VA_ARGS__, __VA_ARGS__, __VA_ARGS__, __VA_ARGS__, __VA_ARGS__, __VA_ARGS__ 22 | unsigned char checkers_data[] = { REPEAT8(REPEAT8(0xCC, 0xCC, 0xCC, 0xFF), REPEAT8(0x80, 0x80, 0x80, 0xFF)), 23 | REPEAT8(REPEAT8(0x80, 0x80, 0x80, 0xFF), REPEAT8(0xCC, 0xCC, 0xCC, 0xFF)) }; 24 | 25 | struct imv_canvas { 26 | cairo_surface_t *surface; 27 | cairo_t *cairo; 28 | PangoFontDescription *font; 29 | GLuint texture; 30 | int width; 31 | int height; 32 | struct { 33 | struct imv_bitmap *bitmap; 34 | GLuint texture; 35 | } cache; 36 | GLuint checkers_texture; 37 | }; 38 | 39 | struct imv_canvas *imv_canvas_create(int width, int height) 40 | { 41 | struct imv_canvas *canvas = calloc(1, sizeof *canvas); 42 | canvas->surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 43 | width, height); 44 | assert(canvas->surface); 45 | canvas->cairo = cairo_create(canvas->surface); 46 | assert(canvas->cairo); 47 | 48 | canvas->font = pango_font_description_new(); 49 | assert(canvas->font); 50 | 51 | glGenTextures(1, &canvas->texture); 52 | assert(canvas->texture); 53 | 54 | glGenTextures(1, &canvas->checkers_texture); 55 | assert(canvas->checkers_texture); 56 | 57 | glBindTexture(GL_TEXTURE_2D, canvas->checkers_texture); 58 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 59 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 60 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 61 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); 62 | glPixelStorei(GL_UNPACK_ROW_LENGTH, 16); 63 | glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); 64 | glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); 65 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, 66 | GL_UNSIGNED_INT_8_8_8_8_REV, checkers_data); 67 | 68 | canvas->width = width; 69 | canvas->height = height; 70 | 71 | return canvas; 72 | } 73 | 74 | void imv_canvas_free(struct imv_canvas *canvas) 75 | { 76 | if (!canvas) { 77 | return; 78 | } 79 | pango_font_description_free(canvas->font); 80 | canvas->font = NULL; 81 | cairo_destroy(canvas->cairo); 82 | canvas->cairo = NULL; 83 | cairo_surface_destroy(canvas->surface); 84 | canvas->surface = NULL; 85 | glDeleteTextures(1, &canvas->texture); 86 | if (canvas->cache.texture) { 87 | glDeleteTextures(1, &canvas->cache.texture); 88 | } 89 | glDeleteTextures(1, &canvas->checkers_texture); 90 | free(canvas); 91 | } 92 | 93 | void imv_canvas_resize(struct imv_canvas *canvas, int width, int height, double scale) 94 | { 95 | cairo_destroy(canvas->cairo); 96 | cairo_surface_destroy(canvas->surface); 97 | 98 | canvas->width = width; 99 | canvas->height = height; 100 | 101 | canvas->surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 102 | canvas->width, canvas->height); 103 | assert(canvas->surface); 104 | cairo_surface_set_device_scale(canvas->surface, scale, scale); 105 | canvas->cairo = cairo_create(canvas->surface); 106 | assert(canvas->cairo); 107 | } 108 | 109 | void imv_canvas_clear(struct imv_canvas *canvas) 110 | { 111 | cairo_save(canvas->cairo); 112 | cairo_set_source_rgba(canvas->cairo, 0, 0, 0, 0); 113 | cairo_set_operator(canvas->cairo, CAIRO_OPERATOR_SOURCE); 114 | cairo_paint(canvas->cairo); 115 | cairo_restore(canvas->cairo); 116 | } 117 | 118 | void imv_canvas_color(struct imv_canvas *canvas, float r, float g, float b, float a) 119 | { 120 | cairo_set_source_rgba(canvas->cairo, r, g, b, a); 121 | } 122 | 123 | void imv_canvas_fill_rectangle(struct imv_canvas *canvas, int x, int y, int width, int height) 124 | { 125 | cairo_rectangle(canvas->cairo, x, y, width, height); 126 | cairo_fill(canvas->cairo); 127 | } 128 | 129 | void imv_canvas_fill(struct imv_canvas *canvas) 130 | { 131 | cairo_rectangle(canvas->cairo, 0, 0, canvas->width, canvas->height); 132 | cairo_fill(canvas->cairo); 133 | } 134 | 135 | void imv_canvas_fill_checkers(struct imv_canvas *canvas, struct imv_image *image, 136 | int bx, int by, double scale, 137 | double rotation, bool mirrored) 138 | { 139 | GLint viewport[4]; 140 | glGetIntegerv(GL_VIEWPORT, viewport); 141 | 142 | glPushMatrix(); 143 | glOrtho(0.0, viewport[2], viewport[3], 0.0, 0.0, 10.0); 144 | 145 | glBindTexture(GL_TEXTURE_2D, canvas->checkers_texture); 146 | glEnable(GL_TEXTURE_2D); 147 | 148 | const int left = bx; 149 | const int top = by; 150 | const int right = left + imv_image_width(image) * scale; 151 | const int bottom = top + imv_image_height(image) * scale; 152 | const int center_x = left + imv_image_width(image) * scale / 2; 153 | const int center_y = top + imv_image_height(image) * scale / 2; 154 | const float s = (right - left) / 16.0; 155 | const float t = s * imv_image_height(image) / imv_image_width(image); 156 | 157 | glTranslated(center_x, center_y, 0); 158 | if (mirrored) { 159 | glScaled(-1, 1, 1); 160 | } 161 | glRotated(rotation, 0, 0, 1); 162 | glTranslated(-center_x, -center_y, 0); 163 | 164 | glBegin(GL_TRIANGLE_FAN); 165 | glTexCoord2f(0, 0); glVertex2i(left, top); 166 | glTexCoord2f(s, 0); glVertex2i(right, top); 167 | glTexCoord2f(s, t); glVertex2i(right, bottom); 168 | glTexCoord2f(0, t); glVertex2i(left, bottom); 169 | glEnd(); 170 | 171 | glBindTexture(GL_TEXTURE_2D, 0); 172 | glDisable(GL_TEXTURE_2D); 173 | glPopMatrix(); 174 | } 175 | 176 | void imv_canvas_font(struct imv_canvas *canvas, const char *name, int size) 177 | { 178 | pango_font_description_set_family(canvas->font, name); 179 | pango_font_description_set_weight(canvas->font, PANGO_WEIGHT_NORMAL); 180 | pango_font_description_set_size(canvas->font, size * PANGO_SCALE); 181 | } 182 | 183 | PangoLayout *imv_canvas_make_layout(struct imv_canvas *canvas, const char *line) 184 | { 185 | PangoLayout *layout = pango_cairo_create_layout(canvas->cairo); 186 | pango_layout_set_font_description(layout, canvas->font); 187 | pango_layout_set_text(layout, line, -1); 188 | 189 | return layout; 190 | } 191 | 192 | void imv_canvas_show_layout(struct imv_canvas *canvas, int x, int y, 193 | PangoLayout *layout) 194 | { 195 | cairo_move_to(canvas->cairo, x, y); 196 | pango_cairo_show_layout(canvas->cairo, layout); 197 | } 198 | 199 | int imv_canvas_printf(struct imv_canvas *canvas, int x, int y, const char *fmt, ...) 200 | { 201 | char line[1024]; 202 | va_list args; 203 | va_start(args, fmt); 204 | vsnprintf(line, sizeof line, fmt, args); 205 | 206 | PangoLayout *layout = imv_canvas_make_layout(canvas, line); 207 | 208 | imv_canvas_show_layout(canvas, x, y, layout); 209 | 210 | PangoRectangle extents; 211 | pango_layout_get_pixel_extents(layout, NULL, &extents); 212 | 213 | g_object_unref(layout); 214 | 215 | va_end(args); 216 | return extents.width; 217 | } 218 | 219 | void imv_canvas_draw(struct imv_canvas *canvas) 220 | { 221 | GLint viewport[4]; 222 | glGetIntegerv(GL_VIEWPORT, viewport); 223 | glPushMatrix(); 224 | glOrtho(0.0, 1.0, 1.0, 0.0, 0.0, 10.0); 225 | 226 | void *data = cairo_image_surface_get_data(canvas->surface); 227 | 228 | glEnable(GL_TEXTURE_RECTANGLE); 229 | glBindTexture(GL_TEXTURE_RECTANGLE, canvas->texture); 230 | 231 | glPixelStorei(GL_UNPACK_ROW_LENGTH, canvas->width); 232 | glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); 233 | glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); 234 | glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_RGBA8, canvas->width, canvas->height, 235 | 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, data); 236 | 237 | glEnable(GL_BLEND); 238 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 239 | glBegin(GL_TRIANGLE_FAN); 240 | glTexCoord2i(0, 0); glVertex2i(0.0, 0.0); 241 | glTexCoord2i(canvas->width, 0); glVertex2i(1.0, 0.0); 242 | glTexCoord2i(canvas->width, canvas->height); glVertex2i(1.0, 1.0); 243 | glTexCoord2i(0, canvas->height); glVertex2i(0.0, 1.0); 244 | glEnd(); 245 | glDisable(GL_BLEND); 246 | 247 | glBindTexture(GL_TEXTURE_RECTANGLE, 0); 248 | glDisable(GL_TEXTURE_RECTANGLE); 249 | glPopMatrix(); 250 | } 251 | 252 | struct imv_bitmap *imv_image_get_bitmap(const struct imv_image *image); 253 | 254 | static int convert_pixelformat(enum imv_pixelformat fmt) 255 | { 256 | /* opengl uses RGBA order, not ARGB, so we get it to 257 | * flip the bytes around so ARGB -> BGRA 258 | */ 259 | if (fmt == IMV_ARGB) { 260 | return GL_BGRA; 261 | } else if (fmt == IMV_ABGR) { 262 | return GL_RGBA; 263 | } else { 264 | imv_log(IMV_WARNING, "Unknown pixel format. Defaulting to ARGB\n"); 265 | return GL_BGRA; 266 | } 267 | } 268 | 269 | static void draw_bitmap(struct imv_canvas *canvas, 270 | struct imv_bitmap *bitmap, 271 | int bx, int by, double scale, 272 | double rotation, bool mirrored, 273 | enum upscaling_method upscaling_method, 274 | bool cache_invalidated) 275 | { 276 | GLint viewport[4]; 277 | glGetIntegerv(GL_VIEWPORT, viewport); 278 | 279 | glPushMatrix(); 280 | glOrtho(0.0, viewport[2], viewport[3], 0.0, 0.0, 10.0); 281 | 282 | if (!canvas->cache.texture) { 283 | glGenTextures(1, &canvas->cache.texture); 284 | } 285 | 286 | const int format = convert_pixelformat(bitmap->format); 287 | 288 | glBindTexture(GL_TEXTURE_RECTANGLE, canvas->cache.texture); 289 | 290 | GLint upscaling = 0; 291 | if (upscaling_method == UPSCALING_LINEAR) { 292 | upscaling = GL_LINEAR; 293 | } else if (upscaling_method == UPSCALING_NEAREST_NEIGHBOUR) { 294 | upscaling = GL_NEAREST; 295 | } else { 296 | imv_log(IMV_ERROR, "Unknown upscaling method: %d\n", upscaling_method); 297 | abort(); 298 | } 299 | 300 | if (canvas->cache.bitmap != bitmap || cache_invalidated) { 301 | glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, upscaling); 302 | glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, upscaling); 303 | glPixelStorei(GL_UNPACK_ROW_LENGTH, bitmap->width); 304 | glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); 305 | glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); 306 | glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_RGBA8, bitmap->width, bitmap->height, 307 | 0, format, GL_UNSIGNED_INT_8_8_8_8_REV, bitmap->data); 308 | } 309 | canvas->cache.bitmap = bitmap; 310 | 311 | glEnable(GL_TEXTURE_RECTANGLE); 312 | 313 | glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, upscaling); 314 | 315 | const int left = bx; 316 | const int top = by; 317 | const int right = left + bitmap->width * scale; 318 | const int bottom = top + bitmap->height * scale; 319 | const int center_x = left + bitmap->width * scale / 2; 320 | const int center_y = top + bitmap->height * scale / 2; 321 | 322 | glTranslated(center_x, center_y, 0); 323 | if (mirrored) { 324 | glScaled(-1, 1, 1); 325 | } 326 | glRotated(rotation, 0, 0, 1); 327 | glTranslated(-center_x, -center_y, 0); 328 | 329 | glEnable(GL_BLEND); 330 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 331 | 332 | glBegin(GL_TRIANGLE_FAN); 333 | glTexCoord2i(0, 0); glVertex2i(left, top); 334 | glTexCoord2i(bitmap->width, 0); glVertex2i(right, top); 335 | glTexCoord2i(bitmap->width, bitmap->height); glVertex2i(right, bottom); 336 | glTexCoord2i(0, bitmap->height); glVertex2i(left, bottom); 337 | glEnd(); 338 | 339 | glDisable(GL_BLEND); 340 | 341 | glBindTexture(GL_TEXTURE_RECTANGLE, 0); 342 | glDisable(GL_TEXTURE_RECTANGLE); 343 | glPopMatrix(); 344 | } 345 | 346 | #ifdef IMV_BACKEND_LIBRSVG 347 | RsvgHandle *imv_image_get_svg(const struct imv_image *image); 348 | #endif 349 | 350 | void imv_canvas_draw_image(struct imv_canvas *canvas, struct imv_image *image, 351 | int x, int y, double scale, 352 | double rotation, bool mirrored, 353 | enum upscaling_method upscaling_method, 354 | bool cache_invalidated) 355 | { 356 | struct imv_bitmap *bitmap = imv_image_get_bitmap(image); 357 | if (bitmap) { 358 | draw_bitmap(canvas, bitmap, x, y, scale, rotation, mirrored, 359 | upscaling_method, cache_invalidated); 360 | return; 361 | } 362 | 363 | #ifdef IMV_BACKEND_LIBRSVG 364 | RsvgHandle *svg = imv_image_get_svg(image); 365 | if (svg) { 366 | imv_canvas_clear(canvas); 367 | cairo_translate(canvas->cairo, x, y); 368 | cairo_scale(canvas->cairo, scale, scale); 369 | cairo_translate(canvas->cairo, imv_image_width(image) / 2, imv_image_height(image) / 2); 370 | if (mirrored) { 371 | cairo_scale(canvas->cairo, -1, 1); 372 | } 373 | cairo_rotate(canvas->cairo, rotation * M_PI / 180.0); 374 | cairo_translate(canvas->cairo, -imv_image_width(image) / 2, -imv_image_height(image) / 2); 375 | rsvg_handle_render_cairo(svg, canvas->cairo); 376 | cairo_identity_matrix(canvas->cairo); 377 | imv_canvas_draw(canvas); 378 | return; 379 | } 380 | #endif 381 | } 382 | -------------------------------------------------------------------------------- /src/x11_window.c: -------------------------------------------------------------------------------- 1 | #include "window.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | 19 | #include "keyboard.h" 20 | #include "log.h" 21 | 22 | struct imv_window { 23 | Display *x_display; 24 | Window x_window; 25 | GLXContext x_glc; 26 | Atom x_state; 27 | Atom x_fullscreen; 28 | Atom wm_delete_window; 29 | Atom wm_protocols; 30 | int width; 31 | int height; 32 | struct { 33 | struct { 34 | int x, y; 35 | } last; 36 | struct { 37 | int x, y; 38 | } current; 39 | bool mouse1; 40 | } pointer; 41 | 42 | struct imv_keyboard *keyboard; 43 | int pipe_fds[2]; 44 | }; 45 | 46 | static void set_nonblocking(int fd) 47 | { 48 | int flags = fcntl(fd, F_GETFL); 49 | assert(flags != -1); 50 | flags |= O_NONBLOCK; 51 | int rc = fcntl(fd, F_SETFL, flags); 52 | assert(rc != -1); 53 | } 54 | 55 | static void setup_keymap(struct imv_window *window) 56 | { 57 | xcb_connection_t *conn = xcb_connect(NULL, NULL); 58 | if (xcb_connection_has_error(conn)) { 59 | imv_log(IMV_ERROR, "x11_window: Failed to load keymap. Could not connect via xcb."); 60 | return; 61 | } 62 | 63 | if (!xkb_x11_setup_xkb_extension(conn, 64 | XKB_X11_MIN_MAJOR_XKB_VERSION, 65 | XKB_X11_MIN_MINOR_XKB_VERSION, 66 | 0, NULL, NULL, NULL, NULL)) { 67 | xcb_disconnect(conn); 68 | imv_log(IMV_ERROR, "x11_window: Failed to load keymap. xkb extension not supported by server."); 69 | return; 70 | } 71 | 72 | int32_t device = xkb_x11_get_core_keyboard_device_id(conn); 73 | 74 | struct xkb_context *context = xkb_context_new(0); 75 | if (!context) { 76 | xcb_disconnect(conn); 77 | imv_log(IMV_ERROR, "x11_window: Failed to load keymap. Failed to initialise xkb context."); 78 | return; 79 | } 80 | 81 | struct xkb_keymap *keymap = 82 | xkb_x11_keymap_new_from_device(context, conn, device, 0); 83 | if (keymap) { 84 | char *keymap_str = xkb_keymap_get_as_string(keymap, XKB_KEYMAP_USE_ORIGINAL_FORMAT); 85 | imv_keyboard_set_keymap(window->keyboard, keymap_str); 86 | free(keymap_str); 87 | xkb_keymap_unref(keymap); 88 | } else { 89 | imv_log(IMV_ERROR, "x11_window: Failed to load keymap. xkb_x11_keymap_new_from_device returned NULL."); 90 | } 91 | xkb_context_unref(context); 92 | 93 | xcb_disconnect(conn); 94 | } 95 | 96 | struct imv_window *imv_window_create(int w, int h, const char *title) 97 | { 98 | /* Ensure event writes will always be atomic */ 99 | assert(sizeof(struct imv_event) <= PIPE_BUF); 100 | 101 | struct imv_window *window = calloc(1, sizeof *window); 102 | window->pointer.last.x = -1; 103 | window->pointer.last.y = -1; 104 | pipe(window->pipe_fds); 105 | set_nonblocking(window->pipe_fds[0]); 106 | set_nonblocking(window->pipe_fds[1]); 107 | 108 | window->x_display = XOpenDisplay(NULL); 109 | assert(window->x_display); 110 | Window root = DefaultRootWindow(window->x_display); 111 | assert(root); 112 | 113 | GLint att[] = { 114 | GLX_RGBA, 115 | GLX_DEPTH_SIZE, 116 | 24, 117 | GLX_DOUBLEBUFFER, 118 | None 119 | }; 120 | 121 | XVisualInfo *vi = glXChooseVisual(window->x_display, 0, att); 122 | assert(vi); 123 | 124 | Colormap cmap = XCreateColormap(window->x_display, root, 125 | vi->visual, AllocNone); 126 | 127 | XSetWindowAttributes wa = { 128 | .colormap = cmap, 129 | .event_mask = ExposureMask | KeyPressMask | KeyReleaseMask 130 | | ButtonPressMask | ButtonReleaseMask | PointerMotionMask 131 | }; 132 | 133 | window->x_window = XCreateWindow(window->x_display, root, 0, 0, w, h, 134 | 0, vi->depth, InputOutput, vi->visual, CWColormap | CWEventMask, &wa); 135 | 136 | window->x_state = XInternAtom(window->x_display, "_NET_WM_STATE", true); 137 | window->x_fullscreen = XInternAtom(window->x_display, "_NET_WM_STATE_FULLSCREEN", true); 138 | 139 | XClassHint hint = { 140 | .res_name = "imv", 141 | .res_class= "imv", 142 | }; 143 | XSetClassHint(window->x_display, window->x_window, &hint); 144 | XMapWindow(window->x_display, window->x_window); 145 | window->wm_protocols = XInternAtom(window->x_display, "WM_PROTOCOLS", false); 146 | window->wm_delete_window = XInternAtom(window->x_display, "WM_DELETE_WINDOW", false); 147 | XSetWMProtocols(window->x_display, window->x_window, &window->wm_delete_window, 1); 148 | XStoreName(window->x_display, window->x_window, title); 149 | 150 | window->x_glc = glXCreateContext(window->x_display, vi, NULL, GL_TRUE); 151 | assert(window->x_glc); 152 | glXMakeCurrent(window->x_display, window->x_window, window->x_glc); 153 | 154 | window->keyboard = imv_keyboard_create(); 155 | assert(window->keyboard); 156 | 157 | setup_keymap(window); 158 | 159 | XFree(vi); 160 | 161 | return window; 162 | } 163 | 164 | void imv_window_free(struct imv_window *window) 165 | { 166 | imv_keyboard_free(window->keyboard); 167 | close(window->pipe_fds[0]); 168 | close(window->pipe_fds[1]); 169 | glXMakeCurrent(window->x_display, None, NULL); 170 | glXDestroyContext(window->x_display, window->x_glc); 171 | XDestroyWindow(window->x_display, window->x_window); 172 | XCloseDisplay(window->x_display); 173 | free(window); 174 | } 175 | 176 | void imv_window_clear(struct imv_window *window, unsigned char r, 177 | unsigned char g, unsigned char b) 178 | { 179 | (void)window; 180 | glClearColor(r / 255.0f, g / 255.0f, b / 255.0f, 1.0); 181 | glClear(GL_COLOR_BUFFER_BIT); 182 | } 183 | 184 | void imv_window_get_size(struct imv_window *window, int *w, int *h) 185 | { 186 | if (w) { 187 | *w = window->width; 188 | } 189 | if (h) { 190 | *h = window->height; 191 | } 192 | } 193 | 194 | void imv_window_get_framebuffer_size(struct imv_window *window, int *w, int *h) 195 | { 196 | if (w) { 197 | *w = window->width; 198 | } 199 | if (h) { 200 | *h = window->height; 201 | } 202 | } 203 | 204 | void imv_window_set_title(struct imv_window *window, const char *title) 205 | { 206 | XStoreName(window->x_display, window->x_window, title); 207 | } 208 | 209 | bool imv_window_is_fullscreen(struct imv_window *window) 210 | { 211 | size_t count = 0; 212 | Atom type; 213 | int format; 214 | size_t after; 215 | Atom *props = NULL; 216 | XGetWindowProperty(window->x_display, window->x_window, window->x_state, 217 | 0, 1024, False, XA_ATOM, &type, &format, &count, &after, (unsigned char**)&props); 218 | 219 | bool fullscreen = false; 220 | for (size_t i = 0; i < count; ++i) { 221 | if (props[i] == window->x_fullscreen) { 222 | fullscreen = true; 223 | break; 224 | } 225 | } 226 | XFree(props); 227 | return fullscreen; 228 | } 229 | 230 | void imv_window_set_fullscreen(struct imv_window *window, bool fullscreen) 231 | { 232 | Window root = DefaultRootWindow(window->x_display); 233 | XEvent event = { 234 | .xclient = { 235 | .type = ClientMessage, 236 | .window = window->x_window, 237 | .format = 32, 238 | .message_type = window->x_state, 239 | .data = { 240 | .l = { 241 | (fullscreen ? 1 : 0), 242 | window->x_fullscreen, 243 | 0, 244 | 1 245 | } 246 | } 247 | } 248 | }; 249 | XSendEvent(window->x_display, root, False, 250 | SubstructureNotifyMask | SubstructureRedirectMask, &event ); 251 | } 252 | 253 | bool imv_window_get_mouse_button(struct imv_window *window, int button) 254 | { 255 | if (button == 1) { 256 | return window->pointer.mouse1; 257 | } 258 | return false; 259 | } 260 | 261 | void imv_window_get_mouse_position(struct imv_window *window, double *x, double *y) 262 | { 263 | if (x) { 264 | *x = window->pointer.current.x; 265 | } 266 | if (y) { 267 | *y = window->pointer.current.y; 268 | } 269 | } 270 | 271 | void imv_window_present(struct imv_window *window) 272 | { 273 | glXSwapBuffers(window->x_display, window->x_window); 274 | } 275 | 276 | void imv_window_wait_for_event(struct imv_window *window, double timeout) 277 | { 278 | struct pollfd fds[] = { 279 | {.fd = ConnectionNumber(window->x_display), .events = POLLIN}, 280 | {.fd = window->pipe_fds[0], .events = POLLIN} 281 | }; 282 | nfds_t nfds = sizeof fds / sizeof *fds; 283 | 284 | poll(fds, nfds, timeout * 1000); 285 | } 286 | 287 | void imv_window_push_event(struct imv_window *window, struct imv_event *e) 288 | { 289 | /* Push it down the pipe */ 290 | write(window->pipe_fds[1], e, sizeof *e); 291 | } 292 | 293 | static void handle_keyboard(struct imv_window *window, imv_event_handler handler, void *data, const XEvent *xev) 294 | { 295 | imv_keyboard_update_mods(window->keyboard, (int)xev->xkey.state, 0, 0); 296 | 297 | bool pressed = xev->type == KeyPress; 298 | int scancode = xev->xkey.keycode - 8; 299 | imv_keyboard_update_key(window->keyboard, scancode, pressed); 300 | 301 | if (!pressed) { 302 | return; 303 | } 304 | 305 | char keyname[32] = {0}; 306 | imv_keyboard_keyname(window->keyboard, scancode, keyname, sizeof keyname); 307 | 308 | char text[64] = {0}; 309 | imv_keyboard_get_text(window->keyboard, scancode, text, sizeof text); 310 | 311 | char *desc = imv_keyboard_describe_key(window->keyboard, scancode); 312 | if (!desc) { 313 | desc = strdup(""); 314 | } 315 | 316 | struct imv_event e = { 317 | .type = IMV_EVENT_KEYBOARD, 318 | .data = { 319 | .keyboard = { 320 | .scancode = scancode, 321 | .keyname = keyname, 322 | .description = desc, 323 | .text = text, 324 | } 325 | } 326 | }; 327 | 328 | if (handler) { 329 | handler(data, &e); 330 | } 331 | free(desc); 332 | } 333 | 334 | void imv_window_pump_events(struct imv_window *window, imv_event_handler handler, void *data) 335 | { 336 | XEvent xev; 337 | 338 | while (XPending(window->x_display)) { 339 | XNextEvent(window->x_display, &xev); 340 | 341 | if (xev.type == Expose) { 342 | XWindowAttributes wa; 343 | XGetWindowAttributes(window->x_display, window->x_window, &wa); 344 | window->width = wa.width; 345 | window->height = wa.height; 346 | glViewport(0, 0, wa.width, wa.height); 347 | struct imv_event e = { 348 | .type = IMV_EVENT_RESIZE, 349 | .data = { 350 | .resize = { 351 | .width = wa.width, 352 | .height = wa.height, 353 | .buffer_width = wa.width, 354 | .buffer_height = wa.height, 355 | .scale = 1, 356 | } 357 | } 358 | }; 359 | if (handler) { 360 | handler(data, &e); 361 | } 362 | } else if (xev.type == KeyPress || xev.type == KeyRelease) { 363 | handle_keyboard(window, handler, data, &xev); 364 | } else if (xev.type == ButtonPress || xev.type == ButtonRelease) { 365 | if (xev.xbutton.button == Button1) { 366 | window->pointer.mouse1 = xev.type == ButtonPress; 367 | struct imv_event e = { 368 | .type = IMV_EVENT_MOUSE_BUTTON, 369 | .data = { 370 | .mouse_button = { 371 | .button = 1, 372 | .pressed = xev.type == ButtonPress 373 | } 374 | } 375 | }; 376 | if (handler) { 377 | handler(data, &e); 378 | } 379 | } else if (xev.xbutton.button == Button4 || xev.xbutton.button == Button5) { 380 | struct imv_event e = { 381 | .type = IMV_EVENT_MOUSE_SCROLL, 382 | .data = { 383 | .mouse_scroll = { 384 | .dx = 0, 385 | .dy = xev.xbutton.button == Button4 ? -1 : 1 386 | } 387 | } 388 | }; 389 | if (handler) { 390 | handler(data, &e); 391 | } 392 | } 393 | } else if (xev.type == MotionNotify) { 394 | window->pointer.current.x = xev.xmotion.x; 395 | window->pointer.current.y = xev.xmotion.y; 396 | int dx = window->pointer.current.x - window->pointer.last.x; 397 | int dy = window->pointer.current.y - window->pointer.last.y; 398 | if (window->pointer.last.x == -1) { 399 | dx = 0; 400 | } 401 | if (window->pointer.last.y == -1) { 402 | dy = 0; 403 | } 404 | window->pointer.last.x = window->pointer.current.x; 405 | window->pointer.last.y = window->pointer.current.y; 406 | 407 | struct imv_event e = { 408 | .type = IMV_EVENT_MOUSE_MOTION, 409 | .data = { 410 | .mouse_motion = { 411 | .x = window->pointer.current.x, 412 | .y = window->pointer.current.y, 413 | .dx = dx, 414 | .dy = dy 415 | } 416 | } 417 | }; 418 | if (handler) { 419 | handler(data, &e); 420 | } 421 | } else if (xev.type == ClientMessage) { 422 | if (xev.xclient.message_type = window->wm_protocols && xev.xclient.data.l[0] == window->wm_delete_window) { 423 | struct imv_event e = { 424 | .type = IMV_EVENT_CLOSE 425 | }; 426 | imv_window_push_event(window, &e); 427 | } 428 | } 429 | } 430 | 431 | /* Handle any events in the pipe */ 432 | while (1) { 433 | struct imv_event e; 434 | ssize_t len = read(window->pipe_fds[0], &e, sizeof e); 435 | if (len <= 0) { 436 | break; 437 | } 438 | assert(len == sizeof e); 439 | if (handler) { 440 | handler(data, &e); 441 | } 442 | } 443 | } 444 | --------------------------------------------------------------------------------