├── backends ├── webp.mime ├── poppler.mime ├── spectre.mime ├── archive_cbx.mime ├── archive.mime ├── gdkpixbuf.mime ├── libav.mime ├── wand.mime ├── shared-initializer.c ├── webp.c ├── poppler.c ├── spectre.c ├── archive.c ├── archive_cbx.c ├── gdkpixbuf.c ├── wand.c └── libav.c ├── lib ├── update.sh ├── strnatcmp.h ├── thumbnailcache.h ├── filebuffer.h ├── config_parser.h ├── bostree.h ├── strnatcmp.c ├── config_parser.c ├── filebuffer.c ├── bostree.c └── thumbnailcache.c ├── .github └── workflows │ └── ci.yml ├── pqiv.h ├── GNUmakefile ├── configure └── README.markdown /backends/webp.mime: -------------------------------------------------------------------------------- 1 | image/webp 2 | -------------------------------------------------------------------------------- /backends/poppler.mime: -------------------------------------------------------------------------------- 1 | image/pdf 2 | application/pdf 3 | application/x-pdf 4 | -------------------------------------------------------------------------------- /backends/spectre.mime: -------------------------------------------------------------------------------- 1 | application/postscript 2 | image/eps 3 | image/ps 4 | image/x-eps 5 | image/x-ps 6 | -------------------------------------------------------------------------------- /backends/archive_cbx.mime: -------------------------------------------------------------------------------- 1 | application/x-cbz 2 | application/x-ext-cbz 3 | application/x-cbr 4 | application/x-ext-cbr 5 | -------------------------------------------------------------------------------- /backends/archive.mime: -------------------------------------------------------------------------------- 1 | application/x-bzip-compressed-tar 2 | application/x-gzip-compressed-tar 3 | application/zip 4 | application/x-zip 5 | application/x-zip-compressed 6 | application/x-rar 7 | application/x-rar-compressed 8 | -------------------------------------------------------------------------------- /lib/update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | for FILE in \ 3 | "https://raw.githubusercontent.com/sourcefrog/natsort/master/strnatcmp.c" \ 4 | "https://raw.githubusercontent.com/sourcefrog/natsort/master/strnatcmp.h" \ 5 | "https://raw.github.com/phillipberndt/bostree/master/bostree.c" \ 6 | "https://raw.github.com/phillipberndt/bostree/master/bostree.h"; do 7 | 8 | BASENAME=`basename "$FILE"` 9 | wget -O $BASENAME.new $FILE && mv $BASENAME.new $BASENAME 10 | 11 | done 12 | 13 | git diff . || true 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: prepare 12 | run: sudo apt-get update && sudo apt-get install build-essential libgtk-3-dev libmagickwand-dev libarchive-dev libpoppler-glib-dev libavformat-dev libavcodec-dev libswscale-dev libavutil-dev libwebp-dev 13 | - name: configure 14 | run: ./configure 15 | - name: make 16 | run: make 17 | - name: check 18 | run: ./pqiv --version 19 | -------------------------------------------------------------------------------- /backends/gdkpixbuf.mime: -------------------------------------------------------------------------------- 1 | application/x-navi-animation 2 | image/bmp 3 | image/gif 4 | image/jpeg 5 | image/jpg 6 | image/png 7 | image/qtif 8 | image/svg 9 | image/svg-xml 10 | image/svg+xml 11 | image/svg+xml-compressed 12 | image/tiff 13 | image/vnd.adobe.svg+xml 14 | image/x-bmp 15 | image/x-icns 16 | image/x-ico 17 | image/x-icon 18 | image/x-ms-bmp 19 | image/x-MS-bmp 20 | image/x-png 21 | image/x-portable-anymap 22 | image/x-portable-bitmap 23 | image/x-portable-graymap 24 | image/x-portable-pixmap 25 | image/x-quicktime 26 | image/x-tga 27 | image/x-win-bitmap 28 | image/x-wmf 29 | image/x-xbitmap 30 | image/x-xpixmap 31 | text/xml-svg 32 | -------------------------------------------------------------------------------- /backends/libav.mime: -------------------------------------------------------------------------------- 1 | application/x-flash-video 2 | application/x-troff-msvideo 3 | video/3gp 4 | video/3gpp 5 | video/avi 6 | video/divx 7 | video/dv 8 | video/fli 9 | video/flv 10 | video/mp2t 11 | video/mp4 12 | video/mp4v-es 13 | video/mpeg 14 | video/msvideo 15 | video/ogg 16 | video/quicktime 17 | video/vivo 18 | video/vnd.divx 19 | video/vnd.mpegurl 20 | video/vnd.rn-realvideo 21 | video/vnd.vivo 22 | video/webm 23 | video/x-anim 24 | video/x-avi 25 | video/x-flc 26 | video/x-fli 27 | video/x-flic 28 | video/x-flv 29 | video/x-m4v 30 | video/x-matroska 31 | video/x-mpeg 32 | video/x-mpeg2 33 | video/x-mpg 34 | video/x-ms-afs 35 | video/x-ms-asf 36 | video/x-ms-asx 37 | video/x-msvideo 38 | video/x-ms-wm 39 | video/x-ms-wmv 40 | video/x-ms-wmx 41 | video/x-ms-wvx 42 | video/x-ms-wvxvideo 43 | video/x-nsv 44 | video/x-ogm+ogg 45 | video/x-theora 46 | video/x-theora+ogg 47 | video/x-totem-stream 48 | -------------------------------------------------------------------------------- /backends/wand.mime: -------------------------------------------------------------------------------- 1 | image/avi 2 | image/avs 3 | image/bie 4 | image/bmp 5 | image/cmyk 6 | image/dcx 7 | image/eps 8 | image/fax 9 | image/fits 10 | image/g3fax 11 | image/gif 12 | image/gray 13 | image/jp2 14 | image/jpeg 15 | image/jpeg2000 16 | image/jpg 17 | image/jpx 18 | image/miff 19 | image/mono 20 | image/mtv 21 | image/pcd 22 | image/pcx 23 | image/pdf 24 | image/pict 25 | image/pjpeg 26 | image/png 27 | image/ps 28 | image/rad 29 | image/rgba 30 | image/rla 31 | image/rle 32 | image/sgi 33 | image/sun-raster 34 | image/svg+xml 35 | image/svg+xml-compressed 36 | image/targa 37 | image/tiff 38 | image/uyvy 39 | image/vid 40 | image/viff 41 | image/vnd.rn-realpix 42 | image/vnd.wap.wbmp 43 | image/x-bmp 44 | image/x-bzeps 45 | image/x-compressed-xcf 46 | image/x-eps 47 | image/x-fits 48 | image/x-freehand 49 | image/x-gimp-gbr 50 | image/x-gimp-gih 51 | image/x-gimp-pat 52 | image/x-gray 53 | image/x-gzeps 54 | image/x-icb 55 | image/x-ico 56 | image/x-icon 57 | image/x-ms-bmp 58 | image/x-pcx 59 | image/x-pict 60 | image/x-png 61 | image/x-portable-anymap 62 | image/x-portable-bitmap 63 | image/x-portable-graymap 64 | image/x-portable-pixmap 65 | image/x-psd 66 | image/x-psp 67 | image/x-rgb 68 | image/x-sgi 69 | image/x-tga 70 | image/x-wmf 71 | image/x-xbitmap 72 | image/x-xcf 73 | image/x-xcursor 74 | image/x-xpixmap 75 | image/x-xwindowdump 76 | image/xpm 77 | image/yuv 78 | -------------------------------------------------------------------------------- /lib/strnatcmp.h: -------------------------------------------------------------------------------- 1 | /* -*- mode: c; c-file-style: "k&r" -*- 2 | 3 | strnatcmp.c -- Perform 'natural order' comparisons of strings in C. 4 | Copyright (C) 2000, 2004 by Martin Pool 5 | 6 | This software is provided 'as-is', without any express or implied 7 | warranty. In no event will the authors be held liable for any damages 8 | arising from the use of this software. 9 | 10 | Permission is granted to anyone to use this software for any purpose, 11 | including commercial applications, and to alter it and redistribute it 12 | freely, subject to the following restrictions: 13 | 14 | 1. The origin of this software must not be misrepresented; you must not 15 | claim that you wrote the original software. If you use this software 16 | in a product, an acknowledgment in the product documentation would be 17 | appreciated but is not required. 18 | 2. Altered source versions must be plainly marked as such, and must not be 19 | misrepresented as being the original software. 20 | 3. This notice may not be removed or altered from any source distribution. 21 | */ 22 | 23 | 24 | /* CUSTOMIZATION SECTION 25 | * 26 | * You can change this typedef, but must then also change the inline 27 | * functions in strnatcmp.c */ 28 | typedef char nat_char; 29 | 30 | int strnatcmp(nat_char const *a, nat_char const *b); 31 | int strnatcasecmp(nat_char const *a, nat_char const *b); 32 | -------------------------------------------------------------------------------- /lib/thumbnailcache.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of pqiv 3 | * Copyright (c) 2017, Phillip Berndt 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * You should have received a copy of the GNU General Public License 14 | * along with this program. If not, see . 15 | * 16 | * This implements thumbnail caching as specified in 17 | * https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html 18 | */ 19 | 20 | #include "../pqiv.h" 21 | 22 | #ifndef CONFIGURED_WITHOUT_MONTAGE_MODE 23 | typedef enum { THUMBNAILS_PERSIST_OFF, THUMBNAILS_PERSIST_ON, THUMBNAILS_PERSIST_STANDARD, THUMBNAILS_PERSIST_RO, THUMBNAILS_PERSIST_LOCAL } thumbnail_persist_mode_t; 24 | 25 | gboolean load_thumbnail_from_cache(file_t *file, unsigned width, unsigned height, thumbnail_persist_mode_t persist_mode, char *special_thumbnail_directory); 26 | gboolean store_thumbnail_to_cache(file_t *file, unsigned width, unsigned height, thumbnail_persist_mode_t persist_mode, char *special_thumbnail_directory); 27 | #endif 28 | -------------------------------------------------------------------------------- /lib/filebuffer.h: -------------------------------------------------------------------------------- 1 | /** 2 | * pqiv 3 | * 4 | * Copyright (c) 2013-2014, Phillip Berndt 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | */ 18 | 19 | // Utility functions for backends that do not support streams 20 | // 21 | // These functions assure that a file is available in-memory or as 22 | // a local file, regardless of the GIO backend handling it. Through 23 | // reference counting, multi-page documents can be handled with a 24 | // single temporary copy, rather than having to keep one copy per 25 | // page 26 | // 27 | 28 | #include "../pqiv.h" 29 | 30 | // Return a bytes view on a file_t 31 | GBytes *buffered_file_as_bytes(file_t *file, GInputStream *data, GError **error_pointer); 32 | 33 | // Return a (possibly temporary) file for a file_t 34 | char *buffered_file_as_local_file(file_t *file, GInputStream *data, GError **error_pointer); 35 | 36 | // Unreference one of the above, free'ing memory if 37 | // necessary 38 | void buffered_file_unref(file_t *file); 39 | -------------------------------------------------------------------------------- /lib/config_parser.h: -------------------------------------------------------------------------------- 1 | /* A simple INI file parser 2 | * Copyright (c) 2016, Phillip Berndt 3 | * Part of pqiv 4 | * 5 | * 6 | * This is a simple stream based configuration file parser. Whitespace at the 7 | * beginning of a line is ignored, except for the continuation of values. 8 | * Configuration files are separated into sections, marked by [section name]. A 9 | * section may contain key=value associations, or be any text alternatively. 10 | * Values may be continued on the next line by indenting its content. 11 | * "#" and ";" at the beginning of a line mark comments inside key/value sections. 12 | * To remove comments from plain-text sections, config_parser_strip_comments() 13 | * may be used. 14 | * 15 | * The API is simple, call config_parser_parse_data() or 16 | * config_parser_parse_file() with a callback function. This function will be 17 | * called for each section text or key/value association found. 18 | * 19 | * The parser makes sure that the file_data remains unchanged if the .._data 20 | * API is used, but does not restore changes the user performs in the callback. 21 | */ 22 | 23 | #include 24 | 25 | typedef struct { 26 | int intval; 27 | double doubleval; 28 | char *chrpval; 29 | } config_parser_value_t ; 30 | 31 | typedef void (*config_parser_callback_t)(char *section, char *key, config_parser_value_t *value); 32 | 33 | void config_parser_parse_file(const char *file_name, config_parser_callback_t callback); 34 | void config_parser_parse_data(char *file_data, size_t file_length, config_parser_callback_t callback); 35 | 36 | void config_parser_strip_comments(char *p); 37 | #define config_parser_tolower(p) if(p) { for(char *n=p ; *n; ++n) *n = tolower(*n); } 38 | -------------------------------------------------------------------------------- /backends/shared-initializer.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "../pqiv.h" 6 | 7 | #ifndef SHARED_BACKENDS 8 | #warning The SHARED_BACKENDS constant is undefined. Defaulting to only gdkpixbuf support. 9 | #define SHARED_BACKENDS "gdkpixbuf", 10 | #endif 11 | #ifndef SEARCH_PATHS 12 | #warning The SEARCH_PATHS constant is undefined. Defaulting to only backends subdirectory. 13 | #define SEARCH_PATHS "backends", 14 | #endif 15 | 16 | static const char *available_backends[] = { 17 | SHARED_BACKENDS 18 | NULL 19 | }; 20 | static const char *search_paths[] = { 21 | SEARCH_PATHS 22 | NULL 23 | }; 24 | file_type_handler_t file_type_handlers[sizeof(available_backends) / sizeof(char *)]; 25 | 26 | extern char **global_argv; 27 | static char *self_path = NULL; 28 | static gchar *get_backend_path(const gchar *backend_name) { 29 | // We search for the libraries ourselves because with --enable-new-dtags 30 | // (default at least on Gentoo), ld writes the search path to RUNPATH 31 | // instead of RPATH, which dlopen() ignores. 32 | // 33 | #ifdef __linux__ 34 | if(self_path == NULL) { 35 | gchar self_name[PATH_MAX]; 36 | ssize_t name_length = readlink("/proc/self/exe", self_name, PATH_MAX); 37 | if(name_length >= 0) { 38 | self_name[name_length] = 0; 39 | self_path = g_strdup(dirname(self_name)); 40 | } 41 | } 42 | #endif 43 | if(self_path == NULL) { 44 | self_path = g_strdup(dirname(global_argv[0])); 45 | } 46 | 47 | for(char **search_path=(char **)&search_paths[0]; *search_path; search_path++) { 48 | gchar *backend_candidate = g_strdup_printf("%s/%s/pqiv-backend-%s.so", (*search_path)[0] == '/' ? "" : self_path, *search_path, backend_name); 49 | 50 | if(g_file_test(backend_candidate, G_FILE_TEST_IS_REGULAR)) { 51 | return backend_candidate; 52 | } 53 | 54 | g_free(backend_candidate); 55 | } 56 | 57 | // As a fallback, always use the system's library lookup mechanism 58 | return g_strdup_printf("pqiv-backend-%s.so", backend_name); 59 | } 60 | 61 | void initialize_file_type_handlers(const gchar * const * disabled_backends) { 62 | int i = 0; 63 | for(char **backend=(char **)&available_backends[0]; *backend; backend++) { 64 | if(strv_contains(disabled_backends, *backend)) { 65 | continue; 66 | } 67 | 68 | gchar *backend_candidate = get_backend_path(*backend); 69 | 70 | GModule *backend_module = g_module_open(backend_candidate, G_MODULE_BIND_LOCAL); 71 | if(backend_module) { 72 | gchar *backend_initializer = g_strdup_printf("file_type_%s_initializer", *backend); 73 | 74 | file_type_initializer_fn_t initializer; 75 | if(g_module_symbol(backend_module, backend_initializer, (gpointer *)&initializer)) { 76 | initializer(&file_type_handlers[i++]); 77 | g_module_make_resident(backend_module); 78 | } 79 | 80 | g_free(backend_initializer); 81 | g_module_close(backend_module); 82 | } 83 | 84 | g_free(backend_candidate); 85 | } 86 | if(i == 0) { 87 | g_printerr("Failed to load any of the image backends.\n"); 88 | exit(1); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/bostree.h: -------------------------------------------------------------------------------- 1 | /* 2 | Self-Balancing order statistic tree 3 | 4 | Implements an AVL tree with two additional methods, 5 | Select(i), which finds the i'th smallest element, and 6 | Rank(x), which returns the rank of a given element, 7 | which both run in O(log n). 8 | 9 | The tree is implemented with map semantics, that is, there are separete key 10 | and value pointers. 11 | 12 | Copyright (c) 2017, Phillip Berndt 13 | 14 | This program is free software: you can redistribute it and/or modify 15 | it under the terms of the GNU General Public License as published by 16 | the Free Software Foundation, either version 3 of the License, or 17 | (at your option) any later version. 18 | 19 | This program is distributed in the hope that it will be useful, 20 | but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | GNU General Public License for more details. 23 | 24 | You should have received a copy of the GNU General Public License 25 | along with this program. If not, see . 26 | */ 27 | 28 | #ifndef BOSTREE_H 29 | #define BOSTREE_H 30 | 31 | /* Opaque tree structure */ 32 | typedef struct _BOSTree BOSTree; 33 | 34 | /* Node structure */ 35 | struct _BOSNode { 36 | unsigned int left_child_count; 37 | unsigned int right_child_count; 38 | unsigned int depth; 39 | 40 | struct _BOSNode *left_child_node; 41 | struct _BOSNode *right_child_node; 42 | struct _BOSNode *parent_node; 43 | 44 | void *key; 45 | void *data; 46 | 47 | unsigned char weak_ref_count; 48 | unsigned char weak_ref_node_valid; 49 | }; 50 | typedef struct _BOSNode BOSNode; 51 | 52 | /* 53 | Public interface 54 | */ 55 | 56 | /** 57 | * Key comparison function. 58 | * 59 | * Should return a positive value if the second argument is larger than the 60 | * first one, a negative value if the first is larger, and zero exactly if both 61 | * are equal. 62 | */ 63 | typedef int (*BOSTree_cmp_function)(const void *, const void *); 64 | 65 | /** 66 | * Free function for deleted nodes. 67 | * 68 | * This function should free the key and data members of a node. The node 69 | * structure itself is free()d internally by BOSZTree. 70 | */ 71 | typedef void (*BOSTree_free_function)(BOSNode *node); 72 | 73 | /** 74 | * Create a new tree. 75 | * 76 | * The cmp_function is mandatory, but for the free function, you may supply a 77 | * NULL pointer if you do not have any data that needs to be free()d in 78 | * ->key and ->data. 79 | */ 80 | BOSTree *bostree_new(BOSTree_cmp_function cmp_function, BOSTree_free_function free_function); 81 | 82 | /** 83 | * Destroy a tree and all its members. 84 | */ 85 | void bostree_destroy(BOSTree *tree); 86 | 87 | /** 88 | * Return the number of nodes in a tree 89 | */ 90 | unsigned int bostree_node_count(BOSTree *tree); 91 | 92 | /** 93 | * Insert a new member into the tree and return the associated node. 94 | */ 95 | BOSNode *bostree_insert(BOSTree *tree, void *key, void *data); 96 | 97 | /** 98 | * Remove a given node from a tree. 99 | */ 100 | void bostree_remove(BOSTree *tree, BOSNode *node); 101 | 102 | /** 103 | * Weak reference management for nodes. 104 | * 105 | * Nodes have an internal reference counter. They are only free()d after the 106 | * last weak reference has been removed. Calling bostree_node_weak_unref() on a 107 | * node which has been removed from the tree results in the weak reference 108 | * count being decreased, the node being possibly free()d if this has been the 109 | * last weak reference, and a NULL pointer being returned. 110 | */ 111 | BOSNode *bostree_node_weak_ref(BOSNode *node); 112 | 113 | /** 114 | * Weak reference management for nodes. 115 | * See bostree_node_weak_ref() 116 | */ 117 | BOSNode *bostree_node_weak_unref(BOSTree *tree, BOSNode *node); 118 | 119 | /** 120 | * Return a node given a key. NULL is returned if a key is not present in the 121 | * tree. 122 | */ 123 | BOSNode *bostree_lookup(BOSTree *tree, const void *key); 124 | 125 | /** 126 | * Return a node given an index in in-order traversal. Indexing starts at 0. 127 | */ 128 | BOSNode *bostree_select(BOSTree *tree, unsigned int index); 129 | 130 | /** 131 | * Return the next node in in-order traversal, or NULL is node was the last 132 | * node in the tree. 133 | */ 134 | BOSNode *bostree_next_node(BOSNode *node); 135 | 136 | /** 137 | * Return the previous node in in-order traversal, or NULL is node was the first 138 | * node in the tree. 139 | */ 140 | BOSNode *bostree_previous_node(BOSNode *node); 141 | 142 | /** 143 | * Return the rank of a node within it's owning tree. 144 | * 145 | * bostree_select(bostree_rank(node)) == node is always true. 146 | */ 147 | unsigned int bostree_rank(BOSNode *node); 148 | 149 | #if !defined(NDEBUG) && (_BSD_SOURCE || _XOPEN_SOURCE || _POSIX_C_SOURCE >= 200112L) 150 | void bostree_print(BOSTree *tree); 151 | #define bostree_debug(...) fprintf(stderr, __VA_ARGS__) 152 | #else 153 | #define bostree_debug(...) void 154 | #endif 155 | 156 | #endif 157 | -------------------------------------------------------------------------------- /lib/strnatcmp.c: -------------------------------------------------------------------------------- 1 | /* -*- mode: c; c-file-style: "k&r" -*- 2 | 3 | strnatcmp.c -- Perform 'natural order' comparisons of strings in C. 4 | Copyright (C) 2000, 2004 by Martin Pool 5 | 6 | This software is provided 'as-is', without any express or implied 7 | warranty. In no event will the authors be held liable for any damages 8 | arising from the use of this software. 9 | 10 | Permission is granted to anyone to use this software for any purpose, 11 | including commercial applications, and to alter it and redistribute it 12 | freely, subject to the following restrictions: 13 | 14 | 1. The origin of this software must not be misrepresented; you must not 15 | claim that you wrote the original software. If you use this software 16 | in a product, an acknowledgment in the product documentation would be 17 | appreciated but is not required. 18 | 2. Altered source versions must be plainly marked as such, and must not be 19 | misrepresented as being the original software. 20 | 3. This notice may not be removed or altered from any source distribution. 21 | 22 | 23 | This version _is_ altered, namely two returns have been removed from the end 24 | of two functions to make the code compile warning-free with -Wunreachable-code. 25 | */ 26 | 27 | 28 | /* partial change history: 29 | * 30 | * 2004-10-10 mbp: Lift out character type dependencies into macros. 31 | * 32 | * Eric Sosman pointed out that ctype functions take a parameter whose 33 | * value must be that of an unsigned int, even on platforms that have 34 | * negative chars in their default char type. 35 | */ 36 | 37 | #include /* size_t */ 38 | #include 39 | 40 | #include "strnatcmp.h" 41 | 42 | 43 | /* These are defined as macros to make it easier to adapt this code to 44 | * different characters types or comparison functions. */ 45 | static inline int 46 | nat_isdigit(nat_char a) 47 | { 48 | return isdigit((unsigned char) a); 49 | } 50 | 51 | 52 | static inline int 53 | nat_isspace(nat_char a) 54 | { 55 | return isspace((unsigned char) a); 56 | } 57 | 58 | 59 | static inline nat_char 60 | nat_toupper(nat_char a) 61 | { 62 | return toupper((unsigned char) a); 63 | } 64 | 65 | 66 | static int 67 | compare_right(nat_char const *a, nat_char const *b) 68 | { 69 | int bias = 0; 70 | 71 | /* The longest run of digits wins. That aside, the greatest 72 | value wins, but we can't know that it will until we've scanned 73 | both numbers to know that they have the same magnitude, so we 74 | remember it in BIAS. */ 75 | for (;; a++, b++) { 76 | if (!nat_isdigit(*a) && !nat_isdigit(*b)) 77 | return bias; 78 | if (!nat_isdigit(*a)) 79 | return -1; 80 | if (!nat_isdigit(*b)) 81 | return +1; 82 | if (*a < *b) { 83 | if (!bias) 84 | bias = -1; 85 | } else if (*a > *b) { 86 | if (!bias) 87 | bias = +1; 88 | } else if (!*a && !*b) 89 | return bias; 90 | } 91 | 92 | /* never reached: return 0; */ 93 | } 94 | 95 | 96 | static int 97 | compare_left(nat_char const *a, nat_char const *b) 98 | { 99 | /* Compare two left-aligned numbers: the first to have a 100 | different value wins. */ 101 | for (;; a++, b++) { 102 | if (!nat_isdigit(*a) && !nat_isdigit(*b)) 103 | return 0; 104 | if (!nat_isdigit(*a)) 105 | return -1; 106 | if (!nat_isdigit(*b)) 107 | return +1; 108 | if (*a < *b) 109 | return -1; 110 | if (*a > *b) 111 | return +1; 112 | } 113 | 114 | /* never reached: return 0; */ 115 | } 116 | 117 | 118 | static int 119 | strnatcmp0(nat_char const *a, nat_char const *b, int fold_case) 120 | { 121 | int ai, bi; 122 | nat_char ca, cb; 123 | int fractional, result; 124 | 125 | ai = bi = 0; 126 | while (1) { 127 | ca = a[ai]; cb = b[bi]; 128 | 129 | /* skip over leading spaces or zeros */ 130 | while (nat_isspace(ca)) 131 | ca = a[++ai]; 132 | 133 | while (nat_isspace(cb)) 134 | cb = b[++bi]; 135 | 136 | /* process run of digits */ 137 | if (nat_isdigit(ca) && nat_isdigit(cb)) { 138 | fractional = (ca == '0' || cb == '0'); 139 | 140 | if (fractional) { 141 | if ((result = compare_left(a+ai, b+bi)) != 0) 142 | return result; 143 | } else { 144 | if ((result = compare_right(a+ai, b+bi)) != 0) 145 | return result; 146 | } 147 | } 148 | 149 | if (!ca && !cb) { 150 | /* The strings compare the same. Perhaps the caller 151 | will want to call strcmp to break the tie. */ 152 | return 0; 153 | } 154 | 155 | if (fold_case) { 156 | ca = nat_toupper(ca); 157 | cb = nat_toupper(cb); 158 | } 159 | 160 | if (ca < cb) 161 | return -1; 162 | 163 | if (ca > cb) 164 | return +1; 165 | 166 | ++ai; ++bi; 167 | } 168 | } 169 | 170 | 171 | int 172 | strnatcmp(nat_char const *a, nat_char const *b) { 173 | return strnatcmp0(a, b, 0); 174 | } 175 | 176 | 177 | /* Compare, recognizing numeric string and ignoring case. */ 178 | int 179 | strnatcasecmp(nat_char const *a, nat_char const *b) { 180 | return strnatcmp0(a, b, 1); 181 | } 182 | -------------------------------------------------------------------------------- /backends/webp.c: -------------------------------------------------------------------------------- 1 | /** 2 | * pqiv 3 | * 4 | * Copyright (c) 2013-2017, Phillip Berndt 5 | * Copyright (c) 2017, Chen John L 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * 19 | * webp backend 20 | * 21 | */ 22 | 23 | #include "../pqiv.h" 24 | #include "../lib/filebuffer.h" 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | 34 | typedef struct { 35 | cairo_surface_t *rendered_image_surface; 36 | } file_private_data_webp_t; 37 | 38 | BOSNode *file_type_webp_alloc(load_images_state_t state, file_t *file) {/*{{{*/ 39 | file->private = g_slice_new0(file_private_data_webp_t); 40 | BOSNode *first_node = load_images_handle_parameter_add_file(state, file); 41 | return first_node; 42 | }/*}}}*/ 43 | 44 | void file_type_webp_free(file_t *file) {/*{{{*/ 45 | g_slice_free(file_private_data_webp_t, file->private); 46 | }/*}}}*/ 47 | 48 | void file_type_webp_load(file_t *file, GInputStream *data, GError **error_pointer) {/*{{{*/ 49 | file_private_data_webp_t *private = file->private; 50 | 51 | // Reset the rendered_image_surface back to NULL 52 | if(private->rendered_image_surface) { 53 | cairo_surface_destroy(private->rendered_image_surface); 54 | private->rendered_image_surface = NULL; 55 | } 56 | 57 | union { 58 | uint32_t u32; 59 | uint8_t u8arr[4]; 60 | } endian_tester; 61 | endian_tester.u32 = 0x12345678; 62 | 63 | int image_width, image_height; 64 | 65 | gsize image_size; 66 | GBytes *image_bytes = buffered_file_as_bytes(file, data, error_pointer); 67 | if(!image_bytes) { 68 | return; 69 | } 70 | const gchar *image_data = g_bytes_get_data(image_bytes, &image_size); 71 | WebPBitstreamFeatures image_features; 72 | VP8StatusCode webp_retstatus = WebPGetFeatures((const uint8_t*)image_data, image_size, &image_features); 73 | int image_decode_ok = 0; 74 | uint8_t* webp_retptr = NULL; 75 | uint8_t* surface_data = NULL; 76 | int surface_stride = 0; 77 | 78 | if(webp_retstatus == VP8_STATUS_OK) { 79 | image_width = image_features.width; 80 | image_height = image_features.height; 81 | 82 | // Create the surface 83 | private->rendered_image_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, image_width, image_height); 84 | 85 | surface_data = cairo_image_surface_get_data(private->rendered_image_surface); 86 | surface_stride = cairo_image_surface_get_stride(private->rendered_image_surface); 87 | 88 | cairo_surface_flush(private->rendered_image_surface); 89 | if(endian_tester.u8arr[0] == 0x12) { 90 | // We are in big endian 91 | webp_retptr = WebPDecodeARGBInto((const uint8_t*)image_data, image_size, surface_data, surface_stride*image_height*4, surface_stride); 92 | } else { 93 | // We are in little endian 94 | webp_retptr = WebPDecodeBGRAInto((const uint8_t*)image_data, image_size, surface_data, surface_stride*image_height*4, surface_stride); 95 | } 96 | cairo_surface_mark_dirty(private->rendered_image_surface); 97 | if(webp_retptr != NULL) { 98 | image_decode_ok = 1; 99 | } 100 | } 101 | buffered_file_unref(file); 102 | image_data = NULL; 103 | image_size = 0; 104 | 105 | if(!image_decode_ok) { 106 | // Clear the rendered_image_surface if an error occurred 107 | if(private->rendered_image_surface) { 108 | cairo_surface_destroy(private->rendered_image_surface); 109 | private->rendered_image_surface = NULL; 110 | } 111 | 112 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-webp-error"), 1, "Failed to load image %s, malformed webp file", file->file_name); 113 | return; 114 | } 115 | 116 | /* Note that cairo's ARGB32 format requires precomputed alpha, but 117 | * the output from webp is not precomputed. Therefore, we do the 118 | * alpha precomputation below if the file has an alpha channel. 119 | */ 120 | 121 | int i, j; 122 | float fR, fG, fB, fA; 123 | int R, G, B; 124 | uint32_t pixel; 125 | if(image_features.has_alpha) { 126 | for(i = 0; i < image_height; i++) { 127 | for(j = 0; j < image_width; j++) { 128 | memcpy(&pixel, &surface_data[i*surface_stride+j*4], sizeof(uint32_t)); 129 | 130 | // Unpack into float 131 | fR = (pixel&0x0FF)/255.0; 132 | fG = ((pixel>>8)&0x0FF)/255.0; 133 | fB = ((pixel>>16)&0x0FF)/255.0; 134 | fA = ((pixel>>24)&0x0FF)/255.0; 135 | // Casting float to int truncates, so for rounding, we add 0.5 136 | R = (fR*fA*255.0+0.5); 137 | G = (fG*fA*255.0+0.5); 138 | B = (fB*fA*255.0+0.5); 139 | pixel = R | (G<<8) | (B<<16) | (pixel&0xFF000000); 140 | 141 | memcpy(&surface_data[i*surface_stride+j*4], &pixel, sizeof(uint32_t)); 142 | } 143 | } 144 | } 145 | 146 | file->width = image_width; 147 | file->height = image_height; 148 | file->is_loaded = TRUE; 149 | }/*}}}*/ 150 | 151 | void file_type_webp_unload(file_t *file) {/*{{{*/ 152 | file_private_data_webp_t *private = file->private; 153 | 154 | if(private->rendered_image_surface) { 155 | cairo_surface_destroy(private->rendered_image_surface); 156 | private->rendered_image_surface = NULL; 157 | } 158 | }/*}}}*/ 159 | 160 | void file_type_webp_draw(file_t *file, cairo_t *cr) {/*{{{*/ 161 | file_private_data_webp_t *private = file->private; 162 | 163 | if(private->rendered_image_surface) { 164 | cairo_set_source_surface(cr, private->rendered_image_surface, 0, 0); 165 | apply_interpolation_quality(cr); 166 | cairo_paint(cr); 167 | } 168 | }/*}}}*/ 169 | 170 | void file_type_webp_initializer(file_type_handler_t *info) {/*{{{*/ 171 | // Fill the file filter pattern 172 | info->file_types_handled = gtk_file_filter_new(); 173 | gtk_file_filter_add_pattern(info->file_types_handled, "*.webp"); 174 | gtk_file_filter_add_mime_type(info->file_types_handled, "image/webp"); 175 | 176 | // Assign the handlers 177 | info->alloc_fn = file_type_webp_alloc; 178 | info->free_fn = file_type_webp_free; 179 | info->load_fn = file_type_webp_load; 180 | info->unload_fn = file_type_webp_unload; 181 | info->draw_fn = file_type_webp_draw; 182 | }/*}}}*/ 183 | -------------------------------------------------------------------------------- /backends/poppler.c: -------------------------------------------------------------------------------- 1 | /** 2 | * pqiv 3 | * 4 | * Copyright (c) 2013-2017, Phillip Berndt 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | * 18 | * libpoppler backend (PDF support) 19 | */ 20 | 21 | #include "../pqiv.h" 22 | #include "../lib/filebuffer.h" 23 | #include 24 | 25 | typedef struct { 26 | // The page to be displayed 27 | PopplerDocument *document; 28 | PopplerPage *page; 29 | 30 | // The page number, for loading 31 | guint page_number; 32 | } file_private_data_poppler_t; 33 | 34 | BOSNode *file_type_poppler_alloc(load_images_state_t state, file_t *file) {/*{{{*/ 35 | // We have to load the file now to get the number of pages 36 | GError *error_pointer = NULL; 37 | 38 | #if POPPLER_CHECK_VERSION(22, 2, 0) 39 | // Poppler has a bug in its stream loader. The original problem #82630 was fixed in 40 | // http://cgit.freedesktop.org/poppler/poppler/commit/?h=poppler-0.26&id=f94ba85a736b4c90c05e7782939f32506472658e 41 | // and the fix appeared in 0.26.5. But there is another bug, see #96884. 42 | // 43 | GInputStream *data = image_loader_stream_file(file, NULL); 44 | if(!data) { 45 | g_printerr("Failed to load PDF %s: Error while reading file\n", file->display_name); 46 | file_free(file); 47 | return NULL; 48 | } 49 | PopplerDocument *poppler_document = poppler_document_new_from_stream(data, -1, NULL, NULL, &error_pointer); 50 | #else 51 | GBytes *data_bytes = buffered_file_as_bytes(file, NULL, &error_pointer); 52 | if(!data_bytes || error_pointer) { 53 | g_printerr("Failed to load PDF %s: %s\n", file->display_name, error_pointer->message); 54 | g_clear_error(&error_pointer); 55 | file_free(file); 56 | return FALSE_POINTER; 57 | } 58 | gsize data_size; 59 | char *data_ptr = (char *)g_bytes_get_data(data_bytes, &data_size); 60 | PopplerDocument *poppler_document = poppler_document_new_from_data(data_ptr, (int)data_size, NULL, &error_pointer); 61 | #endif 62 | 63 | BOSNode *first_node = NULL; 64 | 65 | if(poppler_document) { 66 | int n_pages = poppler_document_get_n_pages(poppler_document); 67 | g_object_unref(poppler_document); 68 | 69 | for(int n=0; ndisplay_name, n + 1), 73 | g_strdup_printf("%s[%d]", file->sort_name, n + 1)); 74 | new_file->private = g_slice_new0(file_private_data_poppler_t); 75 | ((file_private_data_poppler_t *)new_file->private)->page_number = n; 76 | 77 | if(n == 0) { 78 | first_node = load_images_handle_parameter_add_file(state, new_file); 79 | } 80 | else { 81 | load_images_handle_parameter_add_file(state, new_file); 82 | } 83 | } 84 | } 85 | else if(error_pointer) { 86 | g_printerr("Failed to load PDF %s: %s\n", file->display_name, error_pointer->message); 87 | g_clear_error(&error_pointer); 88 | first_node = FALSE_POINTER; 89 | } 90 | 91 | #if POPPLER_CHECK_VERSION(22, 2, 0) 92 | g_object_unref(data); 93 | #else 94 | buffered_file_unref(file); 95 | #endif 96 | 97 | if(first_node) { 98 | file_free(file); 99 | } 100 | return first_node; 101 | }/*}}}*/ 102 | void file_type_poppler_free(file_t *file) {/*{{{*/ 103 | g_slice_free(file_private_data_poppler_t, file->private); 104 | }/*}}}*/ 105 | void file_type_poppler_load(file_t *file, GInputStream *data, GError **error_pointer) {/*{{{*/ 106 | if(error_pointer) { 107 | *error_pointer = NULL; 108 | } 109 | file_private_data_poppler_t *private = file->private; 110 | 111 | // We need to load the data into memory, because poppler has problems with serving from streams; see above 112 | #if POPPLER_CHECK_VERSION(22, 2, 0) 113 | PopplerDocument *document = poppler_document_new_from_stream(data, -1, NULL, image_loader_cancellable, error_pointer); 114 | #else 115 | GBytes *data_bytes = buffered_file_as_bytes(file, data, error_pointer); 116 | if(!data_bytes || (error_pointer && *error_pointer)) { 117 | return; 118 | } 119 | gsize data_size; 120 | char *data_ptr = (char *)g_bytes_get_data(data_bytes, &data_size); 121 | PopplerDocument *document = poppler_document_new_from_data(data_ptr, (int)data_size, NULL, error_pointer); 122 | #endif 123 | 124 | if(document) { 125 | PopplerPage *page = poppler_document_get_page(document, private->page_number); 126 | 127 | if(page) { 128 | double width, height; 129 | poppler_page_get_size(page, &width, &height); 130 | 131 | file->width = width; 132 | file->height = height; 133 | file->is_loaded = TRUE; 134 | private->page = page; 135 | private->document = document; 136 | return; 137 | } 138 | 139 | g_object_unref(document); 140 | } 141 | 142 | #if !POPPLER_CHECK_VERSION(22, 2, 0) 143 | buffered_file_unref(file); 144 | #endif 145 | }/*}}}*/ 146 | void file_type_poppler_unload(file_t *file) {/*{{{*/ 147 | file_private_data_poppler_t *private = file->private; 148 | if(private->page) { 149 | g_object_unref(private->page); 150 | private->page = NULL; 151 | } 152 | if(private->document) { 153 | g_object_unref(private->document); 154 | private->document = NULL; 155 | 156 | #if !POPPLER_CHECK_VERSION(22, 2, 0) 157 | buffered_file_unref(file); 158 | #endif 159 | } 160 | }/*}}}*/ 161 | void file_type_poppler_draw(file_t *file, cairo_t *cr) {/*{{{*/ 162 | file_private_data_poppler_t *private = (file_private_data_poppler_t *)file->private; 163 | 164 | cairo_set_source_rgb(cr, 1., 1., 1.); 165 | cairo_paint(cr); 166 | apply_interpolation_quality(cr); 167 | poppler_page_render(private->page, cr); 168 | }/*}}}*/ 169 | 170 | void file_type_poppler_initializer(file_type_handler_t *info) {/*{{{*/ 171 | // Fill the file filter pattern 172 | info->file_types_handled = gtk_file_filter_new(); 173 | gtk_file_filter_add_pattern(info->file_types_handled, "*.pdf"); 174 | gtk_file_filter_add_mime_type(info->file_types_handled, "application/pdf"); 175 | 176 | // Assign the handlers 177 | info->alloc_fn = file_type_poppler_alloc; 178 | info->free_fn = file_type_poppler_free; 179 | info->load_fn = file_type_poppler_load; 180 | info->unload_fn = file_type_poppler_unload; 181 | info->draw_fn = file_type_poppler_draw; 182 | }/*}}}*/ 183 | -------------------------------------------------------------------------------- /lib/config_parser.c: -------------------------------------------------------------------------------- 1 | /* A simple INI file parser 2 | * Copyright (c) 2016, Phillip Berndt 3 | * Part of pqiv 4 | */ 5 | 6 | #define _POSIX_C_SOURCE 200809L 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #ifdef _POSIX_VERSION 19 | #define HAS_MMAP 20 | #endif 21 | 22 | #ifdef HAS_MMAP 23 | #include 24 | #endif 25 | 26 | #include "config_parser.h" 27 | 28 | void config_parser_strip_comments(char *p) { 29 | char *k; 30 | int state = 0; 31 | 32 | for(; *p; p++) { 33 | if(*p == '\n') { 34 | state = 0; 35 | } 36 | else if((*p == '#' || *p == ';') && state == 0) { 37 | k = strchr(p, '\n'); 38 | if(k) { 39 | memmove(p, k, strlen(k) + 1); 40 | } 41 | else { 42 | *p = 0; 43 | break; 44 | } 45 | } 46 | else if(*p != '\t' && *p != ' ') { 47 | state = 1; 48 | } 49 | } 50 | } 51 | 52 | void config_parser_parse_file(const char *file_name, config_parser_callback_t callback) { 53 | int fd = open(file_name, O_RDONLY); 54 | if(fd < 0) { 55 | return; 56 | } 57 | 58 | struct stat stat; 59 | if(fstat(fd, &stat) < 0) { 60 | close(fd); 61 | return; 62 | } 63 | 64 | char *file_data = NULL; 65 | 66 | #ifdef HAS_MMAP 67 | file_data = mmap(NULL, stat.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); 68 | if(file_data != MAP_FAILED) { 69 | config_parser_parse_data(file_data, stat.st_size, callback); 70 | munmap(file_data, stat.st_size); 71 | close(fd); 72 | } 73 | return; 74 | #endif 75 | 76 | file_data = malloc(stat.st_size); 77 | if(read(fd, file_data, stat.st_size) == stat.st_size) { 78 | config_parser_parse_data(file_data, stat.st_size, callback); 79 | } 80 | free(file_data); 81 | close(fd); 82 | } 83 | 84 | static void _config_parser_parse_data_invoke_callback(config_parser_callback_t callback, char *section_start, char *section_end, char *key_start, char *key_end, char *data_start, char *data_end) { 85 | while(key_end > key_start && (*key_end == ' ' || *key_end == '\n' || *key_end == '\r' || *key_end == '\t')) { 86 | key_end--; 87 | } 88 | while(data_end > data_start && (*data_end == ' ' || *data_end == '\n' || *data_end == '\r' || *data_end == '\t')) { 89 | data_end--; 90 | } 91 | if(!data_start || data_end < data_start || *data_start == '\0') { 92 | return; 93 | } 94 | 95 | char data_end_restore, section_end_restore, key_end_restore; 96 | data_end_restore = data_end[1]; 97 | data_end[1] = 0; 98 | if(section_end) { 99 | section_end_restore = section_end[1]; 100 | section_end[1] = 0; 101 | } 102 | if(key_end) { 103 | key_end_restore = key_end[1]; 104 | key_end[1] = 0; 105 | } 106 | 107 | config_parser_value_t value; 108 | value.chrpval = data_start; 109 | if(*value.chrpval == 'y' || *value.chrpval == 'Y' || *value.chrpval == 't' || *value.chrpval == 'T') { 110 | value.intval = 1; 111 | } 112 | else { 113 | value.intval = atoi(value.chrpval); 114 | } 115 | value.doubleval = atof(value.chrpval); 116 | 117 | callback(section_start, key_start, &value); 118 | 119 | data_end[1] = data_end_restore; 120 | if(section_end) { 121 | section_end[1] = section_end_restore; 122 | } 123 | if(key_end) { 124 | key_end[1] = key_end_restore; 125 | } 126 | } 127 | 128 | void config_parser_parse_data(char *file_data, size_t file_length, config_parser_callback_t callback) { 129 | enum { DEFAULT, SECTION_IDENTIFIER, COMMENT, VALUE } state = DEFAULT; 130 | int section_had_keys = 0; 131 | char *section_start = NULL, *section_end = NULL, *key_start = NULL, *key_end = NULL, *data_start = NULL, *value_start = NULL; 132 | 133 | data_start = file_data; 134 | char *fp; 135 | for(fp = file_data; *fp; fp++) { 136 | if(*fp == ' ' || *fp == '\t') { 137 | continue; 138 | } 139 | 140 | if(state == DEFAULT) { 141 | if(*fp == '[' && key_start == NULL) { 142 | if(!section_had_keys) { 143 | _config_parser_parse_data_invoke_callback(callback, section_start, section_end, NULL, NULL, data_start, fp - 1); 144 | } 145 | 146 | section_had_keys = 0; 147 | section_start = fp + 1; 148 | data_start = NULL; 149 | key_start = NULL; 150 | state = SECTION_IDENTIFIER; 151 | continue; 152 | } 153 | else if(*fp == ';' || *fp == '#') { 154 | state = COMMENT; 155 | } 156 | else if(*fp == '=' && key_start != NULL) { 157 | key_end = fp - 1; 158 | value_start = NULL; 159 | state = VALUE; 160 | } 161 | else if(*fp == '\r' || *fp == '\n') { 162 | key_start = NULL; 163 | } 164 | else { 165 | if(data_start == NULL) { 166 | data_start = fp; 167 | } 168 | if(key_start == NULL) { 169 | key_start = fp; 170 | } 171 | } 172 | } 173 | else if(state == SECTION_IDENTIFIER) { 174 | if(*fp == ']') { 175 | section_end = fp - 1; 176 | state = DEFAULT; 177 | continue; 178 | } 179 | } 180 | else if(state == COMMENT) { 181 | if(*fp == '\n') { 182 | state = DEFAULT; 183 | } 184 | } 185 | else if(state == VALUE) { 186 | if(value_start == NULL) { 187 | value_start = fp; 188 | } 189 | 190 | if(*fp != '\n') { 191 | continue; 192 | } 193 | if(fp[1] == ' ' || fp[1] == '\t') { 194 | continue; 195 | } 196 | state = DEFAULT; 197 | section_had_keys |= 1; 198 | _config_parser_parse_data_invoke_callback(callback, section_start, section_end, key_start, key_end, value_start, fp - 1); 199 | key_start = NULL; 200 | } 201 | } 202 | 203 | if(state == VALUE) { 204 | _config_parser_parse_data_invoke_callback(callback, section_start, section_end, key_start, key_end, value_start, fp - 1); 205 | } 206 | else if(state != SECTION_IDENTIFIER) { 207 | if(!section_had_keys) { 208 | _config_parser_parse_data_invoke_callback(callback, section_start, section_end, NULL, NULL, data_start, fp - 1); 209 | } 210 | } 211 | else { 212 | fprintf(stderr, "Info: Failed to parse configuration, parsing finished in an unexpected state (%d).\n", state); 213 | } 214 | } 215 | 216 | #ifdef TEST 217 | void test_cb(char *section, char *key, config_parser_value_t *value) { 218 | char dup[strlen(value->chrpval)]; 219 | strcpy(dup, value->chrpval); 220 | config_parser_strip_comments(dup); 221 | 222 | printf("%s.%s: i=%d, f=%f, b=%d, s=\"%s\"\n", section, key, value->intval, value->doubleval, !!value->intval, dup); 223 | } 224 | 225 | int main(int argc, char *argv[]) { 226 | if(argc > 1) { 227 | config_parser_parse_file(argv[1], &test_cb); 228 | } 229 | else { 230 | char *data = malloc(10240); 231 | size_t len = 0; 232 | size_t data_size = 10240; 233 | while(true) { 234 | ssize_t red = read(0, &data[len], data_size - len); 235 | if(red == 0) { 236 | break; 237 | } 238 | len += red; 239 | if(len == data_size) { 240 | data_size = data_size + 10240; 241 | data = realloc(data, data_size); 242 | } 243 | } 244 | data[len] = 0; 245 | 246 | char *data_copy = malloc(data_size); 247 | memcpy(data_copy, data, data_size); 248 | 249 | config_parser_parse_data(data, data_size, &test_cb); 250 | 251 | if(memcmp(data, data_copy, data_size) != 0) { 252 | fprintf(stderr, "Warning: Original data changed while processing!\n"); 253 | } 254 | 255 | free(data); 256 | free(data_copy); 257 | } 258 | } 259 | #endif 260 | -------------------------------------------------------------------------------- /backends/spectre.c: -------------------------------------------------------------------------------- 1 | /** 2 | * pqiv 3 | * 4 | * Copyright (c) 2013-2017, Phillip Berndt 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | * 18 | * libspectre backend (PS support) 19 | */ 20 | 21 | #include "../pqiv.h" 22 | #include "../lib/filebuffer.h" 23 | #include 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | typedef struct { 35 | int page_number; 36 | 37 | struct SpectreDocument *document; 38 | struct SpectrePage *page; 39 | } file_private_data_spectre_t; 40 | 41 | #if defined(__GNUC__) 42 | __attribute__((used)) 43 | #endif 44 | void cmsPluginTHR(void *context, void *plugin) { 45 | // This symbol is required to prevent gs from registering its own memory handler, 46 | // causing a crash if poppler is also used 47 | // 48 | // See http://lists.freedesktop.org/archives/poppler/2014-January/010779.html 49 | // 50 | // Plugin is a structure with a member uint32_t type with offsetof(type) == 4*2, 51 | // which has value 0x6D656D48 == "memH". To verify that no other plugins interfere, 52 | // we check that. 53 | // 54 | // Newer versions of ghostscript also try to set a mutex handler, which has type 55 | // mtzH (which is a typo, mtxH was intended) 56 | // 57 | const uint32_t type = *((uint32_t*)plugin + 2); 58 | if(type != 0x6D656D48 && type != 0x6D747A48) { 59 | #ifdef DEBUG 60 | g_printerr("Warning: cmsPluginTHR call was redirected because of a poppler/gs interaction bug, but was called in an unexpected manner.\n"); 61 | #endif 62 | } 63 | } 64 | 65 | BOSNode *file_type_spectre_alloc(load_images_state_t state, file_t *file) {/*{{{*/ 66 | BOSNode *first_node = FALSE_POINTER; 67 | GError *error_pointer = NULL; 68 | 69 | // Load the document to get the number of pages 70 | struct SpectreDocument *document = spectre_document_new(); 71 | char *file_name = buffered_file_as_local_file(file, NULL, &error_pointer); 72 | if(!file_name) { 73 | g_printerr("Failed to load PS file %s: %s\n", file->file_name, error_pointer->message); 74 | g_clear_error(&error_pointer); 75 | return FALSE_POINTER; 76 | } 77 | spectre_document_load(document, file_name); 78 | if(spectre_document_status(document)) { 79 | g_printerr("Failed to load image %s: %s\n", file->file_name, spectre_status_to_string(spectre_document_status(document))); 80 | spectre_document_free(document); 81 | buffered_file_unref(file); 82 | file_free(file); 83 | return FALSE_POINTER; 84 | } 85 | int n_pages = spectre_document_get_n_pages(document); 86 | spectre_document_free(document); 87 | buffered_file_unref(file); 88 | 89 | for(int n=0; ndisplay_name, n + 1), 93 | g_strdup_printf("%s[%d]", file->sort_name, n + 1)); 94 | new_file->private = g_slice_new0(file_private_data_spectre_t); 95 | ((file_private_data_spectre_t *)new_file->private)->page_number = n; 96 | 97 | if(n == 0) { 98 | first_node = load_images_handle_parameter_add_file(state, new_file); 99 | } 100 | else { 101 | load_images_handle_parameter_add_file(state, new_file); 102 | } 103 | } 104 | 105 | if(first_node) { 106 | file_free(file); 107 | } 108 | return first_node; 109 | }/*}}}*/ 110 | void file_type_spectre_free(file_t *file) {/*{{{*/ 111 | g_slice_free(file_private_data_spectre_t, file->private); 112 | }/*}}}*/ 113 | void file_type_spectre_load(file_t *file, GInputStream *data, GError **error_pointer) {/*{{{*/ 114 | file_private_data_spectre_t *private = file->private; 115 | 116 | gchar *file_name = buffered_file_as_local_file(file, data, error_pointer); 117 | if(!file_name) { 118 | return; 119 | } 120 | struct SpectreDocument *document = spectre_document_new(); 121 | spectre_document_load(document, file_name); 122 | if(spectre_document_status(document)) { 123 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-spectre-error"), 1, "Failed to load image %s: %s\n", file->file_name, spectre_status_to_string(spectre_document_status(private->document))); 124 | buffered_file_unref(file); 125 | return; 126 | } 127 | struct SpectrePage *page = spectre_document_get_page(document, private->page_number); 128 | if(!page) { 129 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-spectre-error"), 1, "Failed to load image %s: Failed to load page %d\n", file->file_name, private->page_number); 130 | spectre_document_free(document); 131 | buffered_file_unref(file); 132 | return; 133 | } 134 | if(spectre_page_status(page)) { 135 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-spectre-error"), 1, "Failed to load image %s / page %d: %s\n", file->file_name, private->page_number, spectre_status_to_string(spectre_page_status(private->page))); 136 | spectre_page_free(page); 137 | spectre_document_free(document); 138 | buffered_file_unref(file); 139 | return; 140 | } 141 | 142 | int width, height; 143 | spectre_page_get_size(page, &width, &height); 144 | file->width = width; 145 | file->height = height; 146 | private->page = page; 147 | private->document = document; 148 | file->is_loaded = TRUE; 149 | }/*}}}*/ 150 | void file_type_spectre_unload(file_t *file) {/*{{{*/ 151 | file_private_data_spectre_t *private = file->private; 152 | 153 | if(private->page) { 154 | spectre_page_free(private->page); 155 | private->page = NULL; 156 | } 157 | if(private->document) { 158 | spectre_document_free(private->document); 159 | private->document = NULL; 160 | 161 | buffered_file_unref(file); 162 | } 163 | }/*}}}*/ 164 | void file_type_spectre_draw(file_t *file, cairo_t *cr) {/*{{{*/ 165 | file_private_data_spectre_t *private = (file_private_data_spectre_t *)file->private; 166 | 167 | SpectreRenderContext *render_context = spectre_render_context_new(); 168 | spectre_render_context_set_scale(render_context, current_scale_level, current_scale_level); 169 | 170 | unsigned char *page_data = NULL; 171 | int row_length; 172 | spectre_page_render(private->page, render_context, &page_data, &row_length); 173 | 174 | spectre_render_context_free(render_context); 175 | 176 | if(spectre_page_status(private->page)) { 177 | g_printerr("Failed to draw image: %s\n", spectre_status_to_string(spectre_page_status(private->page))); 178 | if(page_data) { 179 | g_free(page_data); 180 | } 181 | return; 182 | } 183 | if(page_data == NULL) { 184 | g_printerr("Failed to draw image: Unknown error\n"); 185 | return; 186 | } 187 | 188 | cairo_surface_t *image_surface = cairo_image_surface_create_for_data(page_data, CAIRO_FORMAT_RGB24, file->width * current_scale_level, file->height * current_scale_level, row_length); 189 | 190 | cairo_scale(cr, 1 / current_scale_level, 1 / current_scale_level); 191 | cairo_set_source_surface(cr, image_surface, 0, 0); 192 | apply_interpolation_quality(cr); 193 | cairo_paint(cr); 194 | 195 | cairo_surface_destroy(image_surface); 196 | g_free(page_data); 197 | }/*}}}*/ 198 | 199 | void file_type_spectre_initializer(file_type_handler_t *info) {/*{{{*/ 200 | // Fill the file filter pattern 201 | info->file_types_handled = gtk_file_filter_new(); 202 | gtk_file_filter_add_pattern(info->file_types_handled, "*.ps"); 203 | gtk_file_filter_add_pattern(info->file_types_handled, "*.eps"); 204 | gtk_file_filter_add_mime_type(info->file_types_handled, "application/postscript"); 205 | gtk_file_filter_add_mime_type(info->file_types_handled, "image/x-eps"); 206 | gtk_file_filter_add_mime_type(info->file_types_handled, "image/ps"); 207 | gtk_file_filter_add_mime_type(info->file_types_handled, "image/eps"); 208 | 209 | // Assign the handlers 210 | info->alloc_fn = file_type_spectre_alloc; 211 | info->free_fn = file_type_spectre_free; 212 | info->load_fn = file_type_spectre_load; 213 | info->unload_fn = file_type_spectre_unload; 214 | info->draw_fn = file_type_spectre_draw; 215 | }/*}}}*/ 216 | -------------------------------------------------------------------------------- /lib/filebuffer.c: -------------------------------------------------------------------------------- 1 | /** 2 | * pqiv 3 | * 4 | * Copyright (c) 2013-2014, Phillip Berndt 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | */ 18 | #include "filebuffer.h" 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #ifdef _POSIX_VERSION 28 | #define HAS_MMAP 29 | #endif 30 | 31 | #ifdef HAS_MMAP 32 | #include 33 | #endif 34 | 35 | struct buffered_file { 36 | GBytes *data; 37 | char *file_name; 38 | int ref_count; 39 | gboolean file_name_is_temporary; 40 | }; 41 | 42 | GHashTable *file_buffer_table = NULL; 43 | GRecMutex file_buffer_table_mutex; 44 | 45 | #ifdef HAS_MMAP 46 | extern GFile *gfile_for_commandline_arg(const char *); 47 | 48 | struct buffered_file_mmap_info { 49 | void *ptr; 50 | int fd; 51 | size_t size; 52 | }; 53 | 54 | static void buffered_file_mmap_free_helper(struct buffered_file_mmap_info *info) { 55 | munmap(info->ptr, info->size); 56 | close(info->fd); 57 | g_slice_free(struct buffered_file_mmap_info, info); 58 | } 59 | #endif 60 | 61 | GBytes *buffered_file_as_bytes(file_t *file, GInputStream *data, GError **error_pointer) { 62 | g_rec_mutex_lock(&file_buffer_table_mutex); 63 | if(!file_buffer_table) { 64 | file_buffer_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); 65 | } 66 | struct buffered_file *buffer = g_hash_table_lookup(file_buffer_table, file->file_name); 67 | if(!buffer) { 68 | GBytes *data_bytes = NULL; 69 | 70 | if((file->file_flags & FILE_FLAGS_MEMORY_IMAGE)) { 71 | if(file->file_data_loader) { 72 | data_bytes = file->file_data_loader(file, error_pointer); 73 | } 74 | else { 75 | data_bytes = g_bytes_ref(file->file_data); 76 | } 77 | 78 | if(!data_bytes) { 79 | g_rec_mutex_unlock(&file_buffer_table_mutex); 80 | return NULL; 81 | } 82 | } 83 | else { 84 | 85 | #ifdef HAS_MMAP 86 | // If this is a local file, try to mmap() it first instead of loading it completely 87 | GFile *input_file = gfile_for_commandline_arg(file->file_name); 88 | char *input_file_abspath = g_file_get_path(input_file); 89 | if(input_file_abspath) { 90 | GFileInfo *file_info = g_file_query_info(input_file, G_FILE_ATTRIBUTE_STANDARD_SIZE, G_FILE_QUERY_INFO_NONE, NULL, error_pointer); 91 | if(!file_info) { 92 | g_object_unref(input_file); 93 | g_rec_mutex_unlock(&file_buffer_table_mutex); 94 | return NULL; 95 | } 96 | goffset input_file_size = g_file_info_get_size(file_info); 97 | g_object_unref(file_info); 98 | 99 | int fd = open(input_file_abspath, O_RDONLY); 100 | g_free(input_file_abspath); 101 | if(fd < 0) { 102 | g_object_unref(input_file); 103 | g_rec_mutex_unlock(&file_buffer_table_mutex); 104 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-filebuffer-error"), 1, "Opening the file failed with errno=%d: %s", errno, strerror(errno)); 105 | return NULL; 106 | } 107 | void *input_file_data = mmap(NULL, input_file_size, PROT_READ, MAP_SHARED, fd, 0); 108 | 109 | if(input_file_data != MAP_FAILED) { 110 | struct buffered_file_mmap_info *mmap_info = g_slice_new(struct buffered_file_mmap_info); 111 | mmap_info->ptr = input_file_data; 112 | mmap_info->fd = fd; 113 | mmap_info->size = input_file_size; 114 | 115 | data_bytes = g_bytes_new_with_free_func(input_file_data, input_file_size, (GDestroyNotify)buffered_file_mmap_free_helper, mmap_info); 116 | } 117 | else { 118 | close(fd); 119 | } 120 | } 121 | g_object_unref(input_file); 122 | #endif 123 | 124 | if(data_bytes) { 125 | // mmap() above worked 126 | } 127 | else if(!data) { 128 | data = image_loader_stream_file(file, error_pointer); 129 | if(!data) { 130 | g_rec_mutex_unlock(&file_buffer_table_mutex); 131 | return NULL; 132 | } 133 | data_bytes = g_input_stream_read_completely(data, image_loader_cancellable, error_pointer); 134 | g_object_unref(data); 135 | } 136 | else { 137 | data_bytes = g_input_stream_read_completely(data, image_loader_cancellable, error_pointer); 138 | } 139 | 140 | if(!data_bytes) { 141 | g_rec_mutex_unlock(&file_buffer_table_mutex); 142 | return NULL; 143 | } 144 | } 145 | buffer = g_new0(struct buffered_file, 1); 146 | g_hash_table_insert(file_buffer_table, g_strdup(file->file_name), buffer); 147 | buffer->data = data_bytes; 148 | } 149 | buffer->ref_count++; 150 | g_rec_mutex_unlock(&file_buffer_table_mutex); 151 | return buffer->data; 152 | } 153 | 154 | char *buffered_file_as_local_file(file_t *file, GInputStream *data, GError **error_pointer) { 155 | g_rec_mutex_lock(&file_buffer_table_mutex); 156 | if(!file_buffer_table) { 157 | file_buffer_table = g_hash_table_new(g_str_hash, g_str_equal); 158 | } 159 | struct buffered_file *buffer = g_hash_table_lookup(file_buffer_table, file->file_name); 160 | if(buffer) { 161 | buffer->ref_count++; 162 | g_rec_mutex_unlock(&file_buffer_table_mutex); 163 | return buffer->file_name; 164 | } 165 | 166 | buffer = g_new0(struct buffered_file, 1); 167 | g_hash_table_insert(file_buffer_table, g_strdup(file->file_name), buffer); 168 | 169 | gchar *path = NULL; 170 | if(!(file->file_flags & FILE_FLAGS_MEMORY_IMAGE)) { 171 | GFile *input_file = g_file_new_for_commandline_arg(file->file_name); 172 | path = g_file_get_path(input_file); 173 | g_object_unref(input_file); 174 | } 175 | if(path) { 176 | buffer->file_name = path; 177 | buffer->file_name_is_temporary = FALSE; 178 | } 179 | else { 180 | gboolean local_data = FALSE; 181 | if(!data) { 182 | data = image_loader_stream_file(file, error_pointer); 183 | if(!data) { 184 | g_hash_table_remove(file_buffer_table, file->file_name); 185 | g_rec_mutex_unlock(&file_buffer_table_mutex); 186 | return NULL; 187 | } 188 | local_data = TRUE; 189 | } 190 | 191 | GFile *temporary_file; 192 | GFileIOStream *iostream = NULL; 193 | gchar *extension = strrchr(file->file_name, '.'); 194 | if(extension) { 195 | gchar *name_template = g_strdup_printf("pqiv-XXXXXX%s", extension); 196 | temporary_file = g_file_new_tmp(name_template, &iostream, error_pointer); 197 | g_free(name_template); 198 | } 199 | else { 200 | temporary_file = g_file_new_tmp("pqiv-XXXXXX.ps", &iostream, error_pointer); 201 | } 202 | if(!temporary_file) { 203 | g_printerr("Failed to buffer %s: Could not create a temporary file in %s\n", file->file_name, g_get_tmp_dir()); 204 | if(local_data) { 205 | g_object_unref(data); 206 | } 207 | g_hash_table_remove(file_buffer_table, file->file_name); 208 | g_rec_mutex_unlock(&file_buffer_table_mutex); 209 | return NULL; 210 | } 211 | 212 | if(g_output_stream_splice(g_io_stream_get_output_stream(G_IO_STREAM(iostream)), data, G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, image_loader_cancellable, error_pointer) < 0) { 213 | g_hash_table_remove(file_buffer_table, file->file_name); 214 | if(local_data) { 215 | g_object_unref(data); 216 | } 217 | g_rec_mutex_unlock(&file_buffer_table_mutex); 218 | return NULL; 219 | } 220 | 221 | buffer->file_name = g_file_get_path(temporary_file); 222 | buffer->file_name_is_temporary = TRUE; 223 | 224 | g_object_unref(iostream); 225 | g_object_unref(temporary_file); 226 | if(local_data) { 227 | g_object_unref(data); 228 | } 229 | } 230 | 231 | buffer->ref_count++; 232 | g_rec_mutex_unlock(&file_buffer_table_mutex); 233 | return buffer->file_name; 234 | } 235 | 236 | void buffered_file_unref(file_t *file) { 237 | g_rec_mutex_lock(&file_buffer_table_mutex); 238 | struct buffered_file *buffer = g_hash_table_lookup(file_buffer_table, file->file_name); 239 | if(!buffer) { 240 | g_rec_mutex_unlock(&file_buffer_table_mutex); 241 | return; 242 | } 243 | if(--buffer->ref_count == 0) { 244 | if(buffer->data) { 245 | g_bytes_unref(buffer->data); 246 | } 247 | if(buffer->file_name) { 248 | if(buffer->file_name_is_temporary) { 249 | g_unlink(buffer->file_name); 250 | } 251 | g_free(buffer->file_name); 252 | } 253 | g_hash_table_remove(file_buffer_table, file->file_name); 254 | } 255 | g_rec_mutex_unlock(&file_buffer_table_mutex); 256 | } 257 | -------------------------------------------------------------------------------- /backends/archive.c: -------------------------------------------------------------------------------- 1 | /** 2 | * pqiv 3 | * 4 | * Copyright (c) 2013-2017, Phillip Berndt 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | * 18 | * libarchive backend 19 | * 20 | * This is the non-comicbook variant that handles arbitrary archives 21 | * (recursively, if necessary). 22 | * 23 | */ 24 | 25 | #include "../pqiv.h" 26 | #include "../lib/filebuffer.h" 27 | #include 28 | #include 29 | #include 30 | 31 | typedef struct { 32 | // The source archive 33 | file_t *source_archive; 34 | 35 | // The path to the target file within the archive 36 | gchar *entry_name; 37 | } file_loader_delegate_archive_t; 38 | 39 | static struct archive *file_type_archive_gen_archive(GBytes *data) {/*{{{*/ 40 | struct archive *archive = archive_read_new(); 41 | archive_read_support_format_zip(archive); 42 | archive_read_support_format_rar(archive); 43 | archive_read_support_format_7zip(archive); 44 | archive_read_support_format_tar(archive); 45 | archive_read_support_filter_all(archive); 46 | 47 | gsize data_size; 48 | char *data_ptr = (char *)g_bytes_get_data(data, &data_size); 49 | 50 | if(archive_read_open_memory(archive, data_ptr, data_size) != ARCHIVE_OK) { 51 | g_printerr("Failed to load archive: %s\n", archive_error_string(archive)); 52 | archive_read_free(archive); 53 | return NULL; 54 | } 55 | 56 | return archive; 57 | }/*}}}*/ 58 | 59 | void file_type_archive_data_free(file_loader_delegate_archive_t *data) {/*{{{*/ 60 | if(data->source_archive) { 61 | file_free(data->source_archive); 62 | data->source_archive = NULL; 63 | } 64 | g_free(data); 65 | }/*}}}*/ 66 | 67 | GBytes *file_type_archive_data_loader(file_t *file, GError **error_pointer) {/*{{{*/ 68 | const file_loader_delegate_archive_t *archive_data = g_bytes_get_data(file->file_data, NULL); 69 | 70 | GBytes *data = buffered_file_as_bytes(archive_data->source_archive, NULL, error_pointer); 71 | if(!data) { 72 | g_printerr("Failed to load archive %s: %s\n", file->display_name, error_pointer && *error_pointer ? (*error_pointer)->message : "Unknown error"); 73 | g_clear_error(error_pointer); 74 | return NULL; 75 | } 76 | 77 | struct archive *archive = file_type_archive_gen_archive(data); 78 | if(!archive) { 79 | buffered_file_unref(file); 80 | return NULL; 81 | } 82 | 83 | // Find the proper entry 84 | size_t entry_size = 0; 85 | void *entry_data = NULL; 86 | 87 | struct archive_entry *entry; 88 | while(archive_read_next_header(archive, &entry) == ARCHIVE_OK) { 89 | if(archive_data->entry_name && strcmp(archive_data->entry_name, archive_entry_pathname(entry)) == 0) { 90 | entry_size = archive_entry_size(entry); 91 | entry_data = g_malloc(entry_size); 92 | 93 | if(archive_read_data(archive, entry_data, entry_size) != (ssize_t)entry_size) { 94 | archive_read_free(archive); 95 | buffered_file_unref(file); 96 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-archive-error"), 1, "The file had an unexpected size"); 97 | return NULL; 98 | } 99 | 100 | break; 101 | } 102 | } 103 | 104 | archive_read_free(archive); 105 | buffered_file_unref(archive_data->source_archive); 106 | if(!entry_size) { 107 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-archive-error"), 1, "The file has gone within the archive"); 108 | return NULL; 109 | } 110 | 111 | return g_bytes_new_take(entry_data, entry_size); 112 | }/*}}}*/ 113 | 114 | BOSNode *file_type_archive_alloc(load_images_state_t state, file_t *file) {/*{{{*/ 115 | GError *error_pointer = NULL; 116 | GBytes *data = buffered_file_as_bytes(file, NULL, &error_pointer); 117 | if(!data) { 118 | g_printerr("Failed to load archive %s: %s\n", file->display_name, error_pointer ? error_pointer->message : "Unknown error"); 119 | g_clear_error(&error_pointer); 120 | file_free(file); 121 | return FALSE_POINTER; 122 | } 123 | 124 | struct archive *archive = file_type_archive_gen_archive(data); 125 | if(!archive) { 126 | buffered_file_unref(file); 127 | file_free(file); 128 | return FALSE_POINTER; 129 | } 130 | 131 | GtkFileFilterInfo file_filter_info; 132 | file_filter_info.contains = GTK_FILE_FILTER_FILENAME | GTK_FILE_FILTER_DISPLAY_NAME; 133 | 134 | BOSNode *first_node = FALSE_POINTER; 135 | 136 | struct archive_entry *entry; 137 | while(archive_read_next_header(archive, &entry) == ARCHIVE_OK) { 138 | const gchar *entry_name = archive_entry_pathname(entry); 139 | 140 | #if ARCHIVE_VERSION_NUMBER < 3003002 141 | // Affected by libarchive bug #869 142 | if(archive_entry_size(entry) == 0) { 143 | const char *archive_format = archive_format_name(archive); 144 | if(strncmp("ZIP", archive_format, 3) == 0) { 145 | g_printerr("Failed to load archive %s: This ZIP file is affected by libarchive bug #869, which was fixed in v3.3.2. Skipping file.\n", file->display_name); 146 | archive_read_free(archive); 147 | buffered_file_unref(file); 148 | file_free(file); 149 | return FALSE_POINTER; 150 | } 151 | } 152 | #endif 153 | 154 | 155 | // Prepare a new file_t for this entry 156 | gchar *sub_name = g_strdup_printf("%s#%s", file->display_name, entry_name); 157 | file_t *new_file = image_loader_duplicate_file(file, g_strdup(sub_name), g_strdup(sub_name), sub_name); 158 | if(new_file->file_data) { 159 | g_bytes_unref(new_file->file_data); 160 | new_file->file_data = NULL; 161 | } 162 | size_t delegate_struct_alloc_size = sizeof(file_loader_delegate_archive_t) + strlen(entry_name) + 2; 163 | file_loader_delegate_archive_t *new_file_data = g_malloc(delegate_struct_alloc_size); 164 | new_file_data->source_archive = image_loader_duplicate_file(file, NULL, NULL, NULL); 165 | new_file_data->entry_name = (char *)(new_file_data) + sizeof(file_loader_delegate_archive_t) + 1; 166 | memcpy(new_file_data->entry_name, entry_name, strlen(entry_name) + 1); 167 | new_file->file_data = g_bytes_new_with_free_func(new_file_data, delegate_struct_alloc_size, (GDestroyNotify)file_type_archive_data_free, new_file_data); 168 | new_file->file_flags |= FILE_FLAGS_MEMORY_IMAGE; 169 | new_file->file_data_loader = file_type_archive_data_loader; 170 | 171 | // Find an appropriate handler for this file 172 | gchar *name_lowerc = g_utf8_strdown(entry_name, -1); 173 | file_filter_info.filename = file_filter_info.display_name = name_lowerc; 174 | 175 | // Check if one of the file type handlers can handle this file 176 | BOSNode *node = load_images_handle_parameter_find_handler(entry_name, state, new_file, &file_filter_info); 177 | if(node == NULL) { 178 | // No handler found. We could fall back to using a default. Free new_file instead. 179 | file_free(new_file); 180 | } 181 | else if(node == FALSE_POINTER) { 182 | // File type is known, but loading failed; new_file has already been free()d 183 | node = NULL; 184 | } 185 | else if(first_node == FALSE_POINTER) { 186 | first_node = node; 187 | } 188 | 189 | g_free(name_lowerc); 190 | 191 | archive_read_data_skip(archive); 192 | } 193 | 194 | archive_read_free(archive); 195 | buffered_file_unref(file); 196 | file_free(file); 197 | return first_node; 198 | }/*}}}*/ 199 | 200 | void file_type_archive_initializer(file_type_handler_t *info) {/*{{{*/ 201 | // Fill the file filter pattern 202 | info->file_types_handled = gtk_file_filter_new(); 203 | 204 | // Mime types for archives 205 | gtk_file_filter_add_mime_type(info->file_types_handled, "application/x-tar"); 206 | gtk_file_filter_add_mime_type(info->file_types_handled, "application/x-zip"); 207 | gtk_file_filter_add_mime_type(info->file_types_handled, "application/x-rar"); 208 | 209 | // Arbitrary archive files 210 | gtk_file_filter_add_pattern(info->file_types_handled, "*.zip"); 211 | gtk_file_filter_add_pattern(info->file_types_handled, "*.rar"); 212 | gtk_file_filter_add_pattern(info->file_types_handled, "*.7z"); 213 | gtk_file_filter_add_pattern(info->file_types_handled, "*.tar"); 214 | gtk_file_filter_add_pattern(info->file_types_handled, "*.tbz"); 215 | gtk_file_filter_add_pattern(info->file_types_handled, "*.tgz"); 216 | gtk_file_filter_add_pattern(info->file_types_handled, "*.tar.bz2"); 217 | gtk_file_filter_add_pattern(info->file_types_handled, "*.tar.gz"); 218 | 219 | // Assign the handlers 220 | info->alloc_fn = file_type_archive_alloc; 221 | }/*}}}*/ 222 | -------------------------------------------------------------------------------- /backends/archive_cbx.c: -------------------------------------------------------------------------------- 1 | /** 2 | * pqiv 3 | * 4 | * Copyright (c) 2013-2017, Phillip Berndt 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | * 18 | * libarchive backend for comic books 19 | * 20 | * This is a stripped down variant of the more advanced archive backend 21 | * which can only handle *.cb? files, archives for comic book storage. 22 | * Such files are guaranteed to contain _only_ jpg/png files, which allows 23 | * to handle them directly using a gdkpixbuf. 24 | * 25 | */ 26 | 27 | #include "../pqiv.h" 28 | #include "../lib/filebuffer.h" 29 | #include 30 | #include 31 | #include 32 | 33 | typedef struct { 34 | // The archive object and raw archive data 35 | gchar *entry_name; 36 | 37 | // The surface where the image is stored. 38 | cairo_surface_t *image_surface; 39 | } file_private_data_archive_t; 40 | 41 | static struct archive *file_type_archive_cbx_gen_archive(GBytes *data) {/*{{{*/ 42 | struct archive *archive = archive_read_new(); 43 | archive_read_support_format_zip(archive); 44 | archive_read_support_format_rar(archive); 45 | archive_read_support_format_7zip(archive); 46 | archive_read_support_format_tar(archive); 47 | archive_read_support_filter_all(archive); 48 | 49 | gsize data_size; 50 | char *data_ptr = (char *)g_bytes_get_data(data, &data_size); 51 | 52 | if(archive_read_open_memory(archive, data_ptr, data_size) != ARCHIVE_OK) { 53 | g_printerr("Failed to load archive: %s\n", archive_error_string(archive)); 54 | archive_read_free(archive); 55 | return NULL; 56 | } 57 | 58 | return archive; 59 | }/*}}}*/ 60 | 61 | BOSNode *file_type_archive_cbx_alloc(load_images_state_t state, file_t *file) {/*{{{*/ 62 | GError *error_pointer = NULL; 63 | GBytes *data = buffered_file_as_bytes(file, NULL, &error_pointer); 64 | if(!data) { 65 | g_printerr("Failed to load archive %s: %s\n", file->display_name, error_pointer ? error_pointer->message : "Unknown error"); 66 | g_clear_error(&error_pointer); 67 | file_free(file); 68 | return FALSE_POINTER; 69 | } 70 | 71 | struct archive *archive = file_type_archive_cbx_gen_archive(data); 72 | if(!archive) { 73 | file_free(file); 74 | return FALSE_POINTER; 75 | } 76 | 77 | BOSNode *first_node = FALSE_POINTER; 78 | 79 | struct archive_entry *entry; 80 | while(archive_read_next_header(archive, &entry) == ARCHIVE_OK) { 81 | const gchar *entry_name = archive_entry_pathname(entry); 82 | 83 | file_t *new_file = image_loader_duplicate_file(file, NULL, g_strdup_printf("%s#%s", file->display_name, entry_name), g_strdup_printf("%s#%s", file->sort_name, entry_name)); 84 | new_file->private = g_slice_new0(file_private_data_archive_t); 85 | ((file_private_data_archive_t *)new_file->private)->entry_name = g_strdup(entry_name); 86 | 87 | if(first_node == FALSE_POINTER) { 88 | first_node = load_images_handle_parameter_add_file(state, new_file); 89 | } 90 | else { 91 | load_images_handle_parameter_add_file(state, new_file); 92 | } 93 | 94 | //printf("%s %d\n", archive_entry_pathname(entry), archive_entry_size(entry)); 95 | archive_read_data_skip(archive); 96 | } 97 | 98 | archive_read_free(archive); 99 | buffered_file_unref(file); 100 | file_free(file); 101 | return first_node; 102 | }/*}}}*/ 103 | void file_type_archive_cbx_free(file_t *file) {/*{{{*/ 104 | if(file->private) { 105 | file_private_data_archive_t *private = (file_private_data_archive_t *)file->private; 106 | 107 | if(private->entry_name) { 108 | g_free(private->entry_name); 109 | private->entry_name = NULL; 110 | } 111 | 112 | g_slice_free(file_private_data_archive_t, file->private); 113 | } 114 | }/*}}}*/ 115 | void file_type_archive_cbx_unload(file_t *file) {/*{{{*/ 116 | file_private_data_archive_t *private = (file_private_data_archive_t *)file->private; 117 | 118 | if(private->image_surface != NULL) { 119 | cairo_surface_destroy(private->image_surface); 120 | private->image_surface = NULL; 121 | } 122 | }/*}}}*/ 123 | gboolean file_type_archive_cbx_load_destroy_old_image_callback(gpointer old_surface) {/*{{{*/ 124 | cairo_surface_destroy((cairo_surface_t *)old_surface); 125 | return FALSE; 126 | }/*}}}*/ 127 | void file_type_archive_cbx_load(file_t *file, GInputStream *data_stream, GError **error_pointer) {/*{{{*/ 128 | file_private_data_archive_t *private = (file_private_data_archive_t *)file->private; 129 | 130 | // Open the archive 131 | GBytes *data = buffered_file_as_bytes(file, data_stream, error_pointer); 132 | if(!data) { 133 | return; 134 | } 135 | 136 | struct archive *archive = file_type_archive_cbx_gen_archive(data); 137 | if(!archive) { 138 | buffered_file_unref(file); 139 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-archive-error"), 1, "Failed to open archive file"); 140 | return; 141 | } 142 | 143 | // Find the proper entry 144 | size_t entry_size = 0; 145 | gchar *entry_data = NULL; 146 | 147 | struct archive_entry *entry; 148 | while(archive_read_next_header(archive, &entry) == ARCHIVE_OK) { 149 | if(private->entry_name && strcmp(private->entry_name, archive_entry_pathname(entry)) == 0) { 150 | entry_size = archive_entry_size(entry); 151 | entry_data = g_malloc(entry_size); 152 | 153 | if(archive_read_data(archive, entry_data, entry_size) != (ssize_t)entry_size) { 154 | archive_read_free(archive); 155 | buffered_file_unref(file); 156 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-archive-error"), 1, "The file had an unexpected size"); 157 | return; 158 | } 159 | 160 | break; 161 | } 162 | } 163 | 164 | archive_read_free(archive); 165 | buffered_file_unref(file); 166 | if(!entry_size) { 167 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-archive-error"), 1, "The file has gone within the archive"); 168 | return; 169 | } 170 | 171 | // Load it as a GdkPixbuf (This could be extended to support animations) 172 | GInputStream *entry_data_stream = g_memory_input_stream_new_from_data(entry_data, entry_size, g_free); 173 | GdkPixbuf *pixbuf = gdk_pixbuf_new_from_stream(entry_data_stream, NULL, error_pointer); 174 | if(!pixbuf) { 175 | g_object_unref(entry_data_stream); 176 | return; 177 | } 178 | g_object_unref(entry_data_stream); 179 | 180 | GdkPixbuf *new_pixbuf = gdk_pixbuf_apply_embedded_orientation(pixbuf); 181 | g_object_unref(pixbuf); 182 | pixbuf = new_pixbuf; 183 | 184 | file->width = gdk_pixbuf_get_width(pixbuf); 185 | file->height = gdk_pixbuf_get_height(pixbuf); 186 | 187 | // Draw to a cairo surface, see gfkpixbuf.c for why this can not use gdk_cairo_surface_create_from_pixbuf. 188 | cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, file->width, file->height); 189 | if(cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) { 190 | g_object_unref(pixbuf); 191 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-archive-error"), 1, "Failed to create a cairo image surface for the loaded image (cairo status %d)\n", cairo_surface_status(surface)); 192 | return; 193 | } 194 | cairo_t *sf_cr = cairo_create(surface); 195 | gdk_cairo_set_source_pixbuf(sf_cr, pixbuf, 0, 0); 196 | cairo_paint(sf_cr); 197 | cairo_destroy(sf_cr); 198 | 199 | cairo_surface_t *old_surface = private->image_surface; 200 | private->image_surface = surface; 201 | if(old_surface != NULL) { 202 | g_idle_add(file_type_archive_cbx_load_destroy_old_image_callback, old_surface); 203 | } 204 | g_object_unref(pixbuf); 205 | 206 | file->is_loaded = TRUE; 207 | }/*}}}*/ 208 | void file_type_archive_cbx_draw(file_t *file, cairo_t *cr) {/*{{{*/ 209 | file_private_data_archive_t *private = (file_private_data_archive_t *)file->private; 210 | 211 | cairo_surface_t *current_image_surface = private->image_surface; 212 | cairo_set_source_surface(cr, current_image_surface, 0, 0); 213 | apply_interpolation_quality(cr); 214 | cairo_paint(cr); 215 | }/*}}}*/ 216 | void file_type_archive_cbx_initializer(file_type_handler_t *info) {/*{{{*/ 217 | // Fill the file filter pattern 218 | info->file_types_handled = gtk_file_filter_new(); 219 | 220 | char pattern[] = { '*', '.', 'c', 'b', '_', '\0' }; 221 | char formats[] = { 'z', 'r', '7', 't', 'a', '\0' }; 222 | for(char *format=formats; *format; format++) { 223 | pattern[4] = *format; 224 | gtk_file_filter_add_pattern(info->file_types_handled, pattern); 225 | } 226 | 227 | // Assign the handlers 228 | info->alloc_fn = file_type_archive_cbx_alloc; 229 | info->free_fn = file_type_archive_cbx_free; 230 | info->load_fn = file_type_archive_cbx_load; 231 | info->unload_fn = file_type_archive_cbx_unload; 232 | info->draw_fn = file_type_archive_cbx_draw; 233 | }/*}}}*/ 234 | -------------------------------------------------------------------------------- /pqiv.h: -------------------------------------------------------------------------------- 1 | /** 2 | * pqiv 3 | * 4 | * Copyright (c) 2013-2017, Phillip Berndt 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | */ 18 | 19 | // This file contains the definition of files, image types and 20 | // the plugin infrastructure. It should be included in file type 21 | // handlers. 22 | 23 | #ifndef _PQIV_H_INCLUDED 24 | #define _PQIV_H_INCLUDED 25 | 26 | #include 27 | #include 28 | #include 29 | #include "lib/bostree.h" 30 | 31 | #ifndef PQIV_VERSION 32 | #define PQIV_VERSION "2.13.3" 33 | #endif 34 | 35 | #define FILE_FLAGS_ANIMATION (guint)(1) 36 | #define FILE_FLAGS_MEMORY_IMAGE (guint)(1<<1) 37 | 38 | #define FALSE_POINTER ((void*)-1) 39 | 40 | // The structure for images {{{ 41 | typedef struct _file file_t; 42 | typedef GBytes *(*file_data_loader_fn_t)(file_t *file, GError **error_pointer); 43 | 44 | typedef struct file_type_handler_struct_t file_type_handler_t; 45 | struct _file { 46 | // File type 47 | const file_type_handler_t *file_type; 48 | 49 | // Special flags 50 | // FILE_FLAGS_ANIMATION -> Animation functions are invoked 51 | // Set by file type handlers 52 | // FILE_FLAGS_MEMORY_IMAGE -> File lives in memory 53 | guint file_flags; 54 | 55 | // The file name to display 56 | // Must be different from file_name, because it is free()d seperately 57 | gchar *display_name; 58 | 59 | // The name to sort by 60 | // Must be set if option_sort is set; in backends the simplest approach 61 | // is to only touch this if it is not NULL 62 | gchar *sort_name; 63 | 64 | // The URI or file name of the file 65 | gchar *file_name; 66 | 67 | // If the file is a memory image, the actual image data _or_ data for the 68 | // file_data_loader callback to use to construct the _actual_ bytes object 69 | // to use. 70 | GBytes *file_data; 71 | 72 | // If the image is a memory image that can be generated at load time, 73 | // store a pointer to the generator. 74 | file_data_loader_fn_t file_data_loader; 75 | 76 | // The file monitor structure is used for inotify-watching of 77 | // the files 78 | GFileMonitor *file_monitor; 79 | 80 | // This flag stores whether this image is currently loaded 81 | // and valid. i.e. if it is set, you can assume that 82 | // private_data contains a representation of the image; 83 | // if not, you can NOT assume that it does not. 84 | gboolean is_loaded; 85 | 86 | // This flag determines whether this file should be reloaded 87 | // despite is_loaded being set 88 | gboolean force_reload; 89 | 90 | // Cached image size 91 | guint width; 92 | guint height; 93 | 94 | #ifndef CONFIGURED_WITHOUT_MONTAGE_MODE 95 | // Cached thumbnail 96 | cairo_surface_t *thumbnail; 97 | #endif 98 | 99 | // Lock to prevent multiple threads from accessing the backend at the same 100 | // time 101 | GMutex lock; 102 | 103 | // Default render, automatically unloaded with the image, not guaranteed to 104 | // be present, not guaranteed to have the correct scale level. 105 | cairo_surface_t *prerendered_view; 106 | 107 | // File-type specific data, allocated and freed by the file type handlers 108 | void *private; 109 | 110 | // TRUE if file is marked 111 | #ifndef CONFIGURED_WITHOUT_EXTERNAL_COMMANDS 112 | gboolean marked; 113 | #endif 114 | }; 115 | // }}} 116 | // Definition of the built-in file types {{{ 117 | 118 | // If you want to implement your own file type, you'll have to implement the 119 | // following functions and a non-static initialization function named 120 | // file_type_NAME_initializer that fills a file_type_handler_t with pointers to 121 | // the functions. Store the file in backends/NAME.c and adjust the Makefile to 122 | // add the required libraries if your backend is listed in the $(BACKENDS) 123 | // variable. 124 | 125 | typedef enum { PARAMETER, RECURSION, INOTIFY, BROWSE_ORIGINAL_PARAMETER, FILTER_OUTPUT } load_images_state_t; 126 | 127 | // Allocation function: Allocate the ->private structure within a file and add the 128 | // image(s) to the list of available images via load_images_handle_parameter_add_file() 129 | // If an image is not to be loaded for any reason, the file structure should be 130 | // deallocated using file_free() 131 | // Returns a pointer to the first added image 132 | // Optional, you can also set the pointer to this function to NULL. 133 | // If new file_t structures are needed, use image_loader_duplicate_file 134 | typedef BOSNode *(*file_type_alloc_fn_t)(load_images_state_t state, file_t *file); 135 | 136 | // Deallocation, if a file is removed from the images list. Free the ->private structure. 137 | // Only called if ->private is non-NULL. 138 | typedef void (*file_type_free_fn_t)(file_t *file); 139 | 140 | // Actually load a file into memory 141 | typedef void (*file_type_load_fn_t)(file_t *file, GInputStream *data, GError **error_pointer); 142 | 143 | // Unload a file 144 | typedef void (*file_type_unload_fn_t)(file_t *file); 145 | 146 | // Animation support: Initialize memory for animations, return ms until first frame 147 | // Optional, you can also set the pointer to this function to NULL. 148 | typedef double (*file_type_animation_initialize_fn_t)(file_t *file); 149 | 150 | // Animation support: Advance to the next frame, return ms until next frame 151 | // Optional, you can also set the pointer to this function to NULL. 152 | typedef double (*file_type_animation_next_frame_fn_t)(file_t *file); 153 | 154 | // Draw the current view to a cairo context 155 | typedef void (*file_type_draw_fn_t)(file_t *file, cairo_t *cr); 156 | 157 | struct file_type_handler_struct_t { 158 | // All files will be filtered with this filter. If it lets it pass, 159 | // a handler is assigned to a file. If none do, the file is 160 | // discarded if it was found during directory traversal or 161 | // loaded using the first image backend if it was an explicit 162 | // parameter. 163 | GtkFileFilter *file_types_handled; 164 | 165 | // Pointers to the functions defined above 166 | file_type_alloc_fn_t alloc_fn; 167 | file_type_free_fn_t free_fn; 168 | file_type_load_fn_t load_fn; 169 | file_type_unload_fn_t unload_fn; 170 | file_type_animation_initialize_fn_t animation_initialize_fn; 171 | file_type_animation_next_frame_fn_t animation_next_frame_fn; 172 | file_type_draw_fn_t draw_fn; 173 | }; 174 | 175 | // Initialization function: Tell pqiv about a backend 176 | typedef void (*file_type_initializer_fn_t)(file_type_handler_t *info); 177 | 178 | // pqiv symbols available to plugins {{{ 179 | 180 | // Global cancellable that should be used for every i/o operation 181 | extern GCancellable *image_loader_cancellable; 182 | 183 | // Current scale level. For backends that don't support cairo natively. 184 | extern gdouble current_scale_level; 185 | 186 | // Load a file from disc/memory/network 187 | GInputStream *image_loader_stream_file(file_t *file, GError **error_pointer); 188 | 189 | // Create a GFile for a file's name (We have a wrapper to support names with colons) 190 | GFile *gfile_for_commandline_arg(const char *parameter); 191 | 192 | // Duplicate a file_t; the private section does not get duplicated, only the pointer gets copied 193 | file_t *image_loader_duplicate_file(file_t *file, gchar *custom_file_name, gchar *custom_display_name, gchar *custom_sort_name); 194 | 195 | // Add a file to the list of loaded files 196 | // Should be called at least once in a file_type_alloc_fn_t, with the state being 197 | // forwarded unaltered. 198 | BOSNode *load_images_handle_parameter_add_file(load_images_state_t state, file_t *file); 199 | 200 | // Find a handler for a given file; useful for handler redirection, see archive 201 | // file type 202 | BOSNode *load_images_handle_parameter_find_handler(const char *param, load_images_state_t state, file_t *file, GtkFileFilterInfo *file_filter_info); 203 | 204 | // Load all data from an input stream into memory, conveinience function 205 | GBytes *g_input_stream_read_completely(GInputStream *input_stream, GCancellable *cancellable, GError **error_pointer); 206 | 207 | // Free a file 208 | void file_free(file_t *file); 209 | 210 | // Set the interpolation filter in a cairo context for the current file based on the user settings 211 | void apply_interpolation_quality(cairo_t *cr); 212 | 213 | // Wrapper for string vector contains function 214 | gboolean strv_contains(const gchar * const *strv, const gchar *str); 215 | 216 | // }}} 217 | 218 | // File type handlers, used in the initializer and file type guessing 219 | extern file_type_handler_t file_type_handlers[]; 220 | 221 | /* }}} */ 222 | 223 | // The means to control pqiv remotely {{{ 224 | typedef enum { 225 | ACTION_NOP, 226 | ACTION_SHIFT_Y, 227 | ACTION_SHIFT_X, 228 | ACTION_SET_SLIDESHOW_INTERVAL_RELATIVE, 229 | ACTION_SET_SLIDESHOW_INTERVAL_ABSOLUTE, 230 | ACTION_SET_SCALE_LEVEL_RELATIVE, 231 | ACTION_SET_SCALE_LEVEL_ABSOLUTE, 232 | ACTION_TOGGLE_SCALE_MODE, 233 | ACTION_SET_SCALE_MODE_SCREEN_FRACTION, 234 | ACTION_TOGGLE_SHUFFLE_MODE, 235 | ACTION_RELOAD, 236 | ACTION_RESET_SCALE_LEVEL, 237 | ACTION_TOGGLE_FULLSCREEN, 238 | ACTION_FLIP_HORIZONTALLY, 239 | ACTION_FLIP_VERTICALLY, 240 | ACTION_ROTATE_LEFT, 241 | ACTION_ROTATE_RIGHT, 242 | ACTION_TOGGLE_INFO_BOX, 243 | ACTION_JUMP_DIALOG, 244 | ACTION_TOGGLE_SLIDESHOW, 245 | ACTION_HARDLINK_CURRENT_IMAGE, 246 | ACTION_GOTO_DIRECTORY_RELATIVE, 247 | ACTION_GOTO_LOGICAL_DIRECTORY_RELATIVE, 248 | ACTION_GOTO_FILE_RELATIVE, 249 | ACTION_QUIT, 250 | ACTION_NUMERIC_COMMAND, 251 | ACTION_COMMAND, 252 | ACTION_ADD_FILE, 253 | ACTION_GOTO_FILE_BYINDEX, 254 | ACTION_GOTO_FILE_BYNAME, 255 | ACTION_REMOVE_FILE_BYINDEX, 256 | ACTION_REMOVE_FILE_BYNAME, 257 | ACTION_OUTPUT_FILE_LIST, 258 | ACTION_SET_CURSOR_VISIBILITY, 259 | ACTION_SET_STATUS_OUTPUT, 260 | ACTION_SET_SCALE_MODE_FIT_PX, 261 | ACTION_SET_SHIFT_X, 262 | ACTION_SET_SHIFT_Y, 263 | ACTION_BIND_KEY, 264 | ACTION_SEND_KEYS, 265 | ACTION_SET_SHIFT_ALIGN_CORNER, 266 | ACTION_SET_INTERPOLATION_QUALITY, 267 | ACTION_ANIMATION_STEP, 268 | ACTION_ANIMATION_CONTINUE, 269 | ACTION_ANIMATION_SET_SPEED_ABSOLUTE, 270 | ACTION_ANIMATION_SET_SPEED_RELATIVE, 271 | ACTION_GOTO_EARLIER_FILE, 272 | ACTION_SET_CURSOR_AUTO_HIDE, 273 | ACTION_SET_FADE_DURATION, 274 | ACTION_SET_KEYBOARD_TIMEOUT, 275 | ACTION_SET_THUMBNAIL_SIZE, 276 | ACTION_SET_THUMBNAIL_PRELOAD, 277 | ACTION_MONTAGE_MODE_ENTER, 278 | ACTION_MONTAGE_MODE_SHIFT_X, 279 | ACTION_MONTAGE_MODE_SHIFT_Y, 280 | ACTION_MONTAGE_MODE_SET_SHIFT_X, 281 | ACTION_MONTAGE_MODE_SET_SHIFT_Y, 282 | ACTION_MONTAGE_MODE_SET_WRAP_MODE, 283 | ACTION_MONTAGE_MODE_SHIFT_Y_PG, 284 | ACTION_MONTAGE_MODE_SHIFT_Y_ROWS, 285 | ACTION_MONTAGE_MODE_SHOW_BINDING_OVERLAYS, 286 | ACTION_MONTAGE_MODE_FOLLOW, 287 | ACTION_MONTAGE_MODE_FOLLOW_PROCEED, 288 | ACTION_MONTAGE_MODE_RETURN_PROCEED, 289 | ACTION_MONTAGE_MODE_RETURN_CANCEL, 290 | ACTION_MOVE_WINDOW, 291 | ACTION_TOGGLE_BACKGROUND_PATTERN, 292 | ACTION_TOGGLE_NEGATE_MODE, 293 | ACTION_TOGGLE_MARK, 294 | ACTION_CLEAR_MARKS, 295 | } pqiv_action_t; 296 | 297 | typedef union { 298 | int pint; 299 | double pdouble; 300 | char *pcharptr; 301 | struct { 302 | short p1; 303 | short p2; 304 | } p2short; 305 | } pqiv_action_parameter_t; 306 | void action(pqiv_action_t action, pqiv_action_parameter_t parameter); 307 | // }}} 308 | 309 | #endif 310 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | # pqiv Makefile 2 | # 3 | 4 | # Default flags, overridden by values in config.make 5 | CFLAGS?=-O2 -g 6 | CROSS= 7 | DESTDIR= 8 | GTK_VERSION=0 9 | PQIV_WARNING_FLAGS=-Wall -Wextra -Wfloat-equal -Wpointer-arith -Wcast-align -Wstrict-overflow=1 -Wwrite-strings -Waggregate-return -Wunreachable-code -Wno-unused-parameter 10 | LDLIBS=-lm 11 | PREFIX=/usr 12 | EPREFIX=$(PREFIX) 13 | LIBDIR=$(PREFIX)/lib 14 | BINDIR=$(PREFIX)/bin 15 | MANDIR=$(PREFIX)/share/man 16 | EXECUTABLE_EXTENSION= 17 | PKG_CONFIG=$(CROSS)pkg-config 18 | OBJECTS=pqiv.o lib/strnatcmp.o lib/bostree.o lib/filebuffer.o lib/config_parser.o lib/thumbnailcache.o 19 | HEADERS=pqiv.h lib/bostree.h lib/filebuffer.h lib/strnatcmp.h 20 | BACKENDS=gdkpixbuf 21 | EXTRA_DEFS= 22 | BACKENDS_BUILD=static 23 | EXTRA_CFLAGS_SHARED_OBJECTS=-fPIC 24 | EXTRA_CFLAGS_BINARY= 25 | EXTRA_LDFLAGS_SHARED_OBJECTS= 26 | EXTRA_LDFLAGS_BINARY= 27 | 28 | # Always look for source code relative to the directory of this makefile 29 | SOURCEDIR:=$(dir $(abspath $(lastword $(MAKEFILE_LIST)))) 30 | ifeq ($(SOURCEDIR),$(CURDIR)) 31 | SOURCEDIR= 32 | else 33 | HEADERS:=$(patsubst %, $(SOURCEDIR)%, $(HEADERS)) 34 | endif 35 | 36 | # Load config.make (created by configure) 37 | CONFIG_MAKE_NAME=config.make 38 | ifeq ($(wildcard $(CONFIG_MAKE_NAME)),$(CONFIG_MAKE_NAME)) 39 | include $(CONFIG_MAKE_NAME) 40 | HEADERS+=$(CONFIG_MAKE_NAME) 41 | endif 42 | 43 | # First things first: Require at least one backend 44 | ifeq ($(BACKENDS),) 45 | $(error Building pqiv without any backends is unsupported.) 46 | endif 47 | 48 | # pkg-config lines for the main program 49 | LIBS_GENERAL=glib-2.0 >= 2.8 cairo >= 1.6 pango >= 1.10 gio-2.0 50 | LIBS_GTK3=gtk+-3.0 gdk-3.0 51 | LIBS_GTK2=gtk+-2.0 >= 2.6 gdk-2.0 >= 2.8 52 | 53 | # pkg-config libraries for the backends 54 | LIBS_gdkpixbuf=gdk-pixbuf-2.0 >= 2.2 55 | LIBS_poppler=poppler-glib 56 | LIBS_spectre=libspectre 57 | LIBS_wand=MagickWand 58 | LIBS_libav=libavformat libavcodec libswscale libavutil 59 | LIBS_archive_cbx=libarchive gdk-pixbuf-2.0 >= 2.2 60 | LIBS_archive=libarchive 61 | LIBS_webp=libwebp 62 | 63 | # This might be required if you use mingw, and is required as of 64 | # Aug 2014 for mxe, but IMHO shouldn't be required / is a bug in 65 | # poppler (which does not specify this dependency). If it isn't 66 | # or throws an error for you, please report this as a bug: 67 | # 68 | ifeq ($(EXECUTABLE_EXTENSION),.exe) 69 | LDLIBS_poppler+=-llcms2 -lstdc++ 70 | endif 71 | 72 | # If no GTK_VERSION is set, try to auto-determine, with GTK 3 preferred 73 | ifeq ($(GTK_VERSION), 0) 74 | ifeq ($(shell $(PKG_CONFIG) --errors-to-stdout --print-errors "$(LIBS_GTK3)"), ) 75 | override GTK_VERSION=3 76 | else 77 | LIBS=$(LIBS_GTK2) 78 | override GTK_VERSION=2 79 | endif 80 | endif 81 | ifeq ($(GTK_VERSION), 2) 82 | LIBS=$(LIBS_GTK2) 83 | endif 84 | ifeq ($(GTK_VERSION), 3) 85 | LIBS=$(LIBS_GTK3) 86 | endif 87 | LIBS+=$(LIBS_GENERAL) 88 | 89 | # Add platform specific libraries 90 | # GIo for stdin loading, 91 | ifeq ($(EXECUTABLE_EXTENSION), .exe) 92 | LIBS+=gio-windows-2.0 93 | else 94 | LIBS+=gio-unix-2.0 95 | endif 96 | 97 | # We need X11 to workaround a bug, see http://stackoverflow.com/questions/18647475 98 | ifeq ($(filter x11, $(shell $(PKG_CONFIG) --errors-to-stdout --variable=target gtk+-$(GTK_VERSION).0; $(PKG_CONFIG) --errors-to-stdout --variable=targets gtk+-$(GTK_VERSION).0)), x11) 99 | LIBS+=x11 100 | endif 101 | 102 | # Add backend-specific libraries and objects 103 | SHARED_OBJECTS= 104 | SHARED_BACKENDS= 105 | HELPER_OBJECTS= 106 | BACKENDS_INITIALIZER:=backends/initializer 107 | SORTED_BACKENDS=gdkpixbuf webp archive_cbx archive poppler libav wand 108 | define handle-backend 109 | ifneq ($(origin LIBS_$(1)),undefined) 110 | ifneq ($(findstring $(1), $(BACKENDS)),) 111 | ifeq ($(BACKENDS_BUILD), shared) 112 | ifeq ($(shell $(PKG_CONFIG) --errors-to-stdout --print-errors "$(LIBS_$(1))" 2>&1), ) 113 | SHARED_OBJECTS+=backends/pqiv-backend-$(1).so 114 | HELPER_OBJECTS+=backends/$(1).o 115 | BACKENDS_BUILD_CFLAGS_$(1):=$(shell $(PKG_CONFIG) --errors-to-stdout --print-errors --cflags "$(LIBS_$(1))" 2>&1) 116 | BACKENDS_BUILD_LDLIBS_$(1):=$(shell $(PKG_CONFIG) --errors-to-stdout --print-errors --libs "$(LIBS_$(1))" 2>&1) 117 | SHARED_BACKENDS+="$(1)", 118 | endif 119 | else 120 | LIBS+=$(LIBS_$(1)) 121 | OBJECTS+=backends/$(1).o 122 | LDLIBS+=$(LDLIBS_$(1)) 123 | BACKENDS_INITIALIZER:=$(BACKENDS_INITIALIZER)-$(1) 124 | endif 125 | endif 126 | endif 127 | endef 128 | 129 | define handle-extra-backend 130 | ifeq ($(findstring $(1), $(SORTED_BACKENDS)),) 131 | SORTED_BACKENDS+=$(1) 132 | endif 133 | endef 134 | $(foreach BACKEND_C, $(wildcard $(SOURCEDIR)backends/*.c), $(eval $(call handle-extra-backend,$(basename $(notdir $(BACKEND_C)))))) 135 | $(foreach BACKEND, $(SORTED_BACKENDS), $(eval $(call handle-backend,$(BACKEND)))) 136 | PIXBUF_FILTER="gdkpixbuf", 137 | ifeq ($(BACKENDS_BUILD), shared) 138 | OBJECTS+=backends/shared-initializer.o 139 | BACKENDS_BUILD_CFLAGS_shared-initializer=-DSHARED_BACKENDS='$(filter $(PIXBUF_FILTER), $(SHARED_BACKENDS)) $(filter-out $(PIXBUF_FILTER), $(SHARED_BACKENDS))' -DSEARCH_PATHS='"backends", "../$(subst $(PREFIX),,$(LIBDIR))/pqiv", "$(LIBDIR)/pqiv",' 140 | LIBS+=gmodule-2.0 141 | else 142 | OBJECTS+=$(BACKENDS_INITIALIZER).o 143 | endif 144 | 145 | # MagickWand changed their directory structure with version 7, pass the version 146 | # to the build 147 | ifneq ($(findstring wand, $(BACKENDS)),) 148 | backends/wand.o: CFLAGS_REAL+=-DWAND_VERSION=$(shell $(PKG_CONFIG) --modversion MagickWand | awk 'BEGIN { FS="." } { print $$1 }') 149 | endif 150 | 151 | # Add version information to builds from git 152 | PQIV_VERSION_STRING=$(shell [ -d $(SOURCEDIR).git ] && (which git 2>&1 >/dev/null) && git -C "$(SOURCEDIR)" describe --dirty --tags 2>/dev/null) 153 | ifneq ($(PQIV_VERSION_STRING),) 154 | PQIV_VERSION_FLAG=-DPQIV_VERSION=\"$(PQIV_VERSION_STRING)\" 155 | endif 156 | ifdef DEBUG 157 | DEBUG_CFLAGS=-DDEBUG 158 | else 159 | DEBUG_CFLAGS=-DNDEBUG 160 | endif 161 | 162 | # Less verbose output 163 | ifndef VERBOSE 164 | SILENT_CC=@echo " CC " $@; 165 | SILENT_CCLD=@echo " CCLD" $@; 166 | SILENT_GEN=@echo " GEN " $@; 167 | endif 168 | 169 | # Assemble final compiler flags 170 | CFLAGS_REAL=-std=gnu99 $(PQIV_WARNING_FLAGS) $(PQIV_VERSION_FLAG) $(CFLAGS) $(DEBUG_CFLAGS) $(EXTRA_DEFS) $(shell $(PKG_CONFIG) --cflags "$(LIBS)") 171 | LDLIBS_REAL=$(shell $(PKG_CONFIG) --libs "$(LIBS)") $(LDLIBS) 172 | LDFLAGS_REAL=$(LDFLAGS) 173 | 174 | all: pqiv$(EXECUTABLE_EXTENSION) pqiv.desktop $(SHARED_OBJECTS) 175 | .PHONY: get_libs get_available_backends _build_variables clean distclean install uninstall all 176 | .SECONDARY: 177 | 178 | pqiv$(EXECUTABLE_EXTENSION): $(OBJECTS) 179 | $(SILENT_CCLD) $(CROSS)$(CC) $(CPPFLAGS) $(EXTRA_CFLAGS_BINARY) -o $@ $+ $(LDLIBS_REAL) $(LDFLAGS_REAL) $(EXTRA_LDFLAGS_BINARY) 180 | 181 | ifeq ($(BACKENDS_BUILD), shared) 182 | backends/%.o: CFLAGS_REAL+=$(BACKENDS_BUILD_CFLAGS_$(notdir $*)) $(EXTRA_CFLAGS_SHARED_OBJECTS) 183 | 184 | $(SHARED_OBJECTS): backends/pqiv-backend-%.so: backends/%.o 185 | @[ -d backends ] || mkdir -p backends || true 186 | $(SILENT_CCLD) $(CROSS)$(CC) $(CPPFLAGS) $(EXTRA_CFLAGS_SHARED_OBJECTS) -o $@ $+ $(LDLIBS_REAL) $(LDFLAGS_REAL) $(BACKENDS_BUILD_LDLIBS_$*) $(EXTRA_LDFLAGS_SHARED_OBJECTS) -shared 187 | endif 188 | 189 | $(filter-out $(BACKENDS_INITIALIZER).o, $(OBJECTS)) $(HELPER_OBJECTS): %.o: $(SOURCEDIR)%.c $(HEADERS) 190 | @[ -d $(dir $@) ] || mkdir -p $(dir $@) || true 191 | $(SILENT_CC) $(CROSS)$(CC) $(CPPFLAGS) -c -o $@ $(CFLAGS_REAL) $< 192 | 193 | $(BACKENDS_INITIALIZER).o: $(BACKENDS_INITIALIZER).c $(HEADERS) 194 | @[ -d $(dir $@) ] || mkdir -p $(dir $@) || true 195 | $(SILENT_CC) $(CROSS)$(CC) $(CPPFLAGS) -I"$(SOURCEDIR)/lib" -c -o $@ $(CFLAGS_REAL) $< 196 | 197 | $(BACKENDS_INITIALIZER).c: 198 | @[ -d $(dir $(BACKENDS_INITIALIZER)) ] || mkdir -p $(dir $(BACKENDS_INITIALIZER)) || true 199 | @$(foreach BACKEND, $(sort $(BACKENDS)), [ -e $(SOURCEDIR)backends/$(BACKEND).c ] || { echo; echo "Backend $(BACKEND) not found!" >&2; exit 1; };) 200 | $(SILENT_GEN) ( \ 201 | echo '/* Auto-Generated file by Make. */'; \ 202 | echo '#include "../pqiv.h"'; \ 203 | echo "file_type_handler_t file_type_handlers[$(words $(BACKENDS)) + 1];"; \ 204 | $(foreach BACKEND, $(sort $(BACKENDS)), echo "void file_type_$(BACKEND)_initializer(file_type_handler_t *info);";) \ 205 | echo "void initialize_file_type_handlers(const gchar * const * disabled_backends) {"; \ 206 | echo " int i = 0;"; \ 207 | $(foreach BACKEND, $(filter gdkpixbuf, $(BACKENDS)), echo " if(!strv_contains(disabled_backends, \"$(BACKEND)\")) file_type_$(BACKEND)_initializer(&file_type_handlers[i++]);";) \ 208 | $(foreach BACKEND, $(sort $(filter-out gdkpixbuf, $(BACKENDS))), echo " if(!strv_contains(disabled_backends, \"$(BACKEND)\")) file_type_$(BACKEND)_initializer(&file_type_handlers[i++]);";) \ 209 | echo "}" \ 210 | ) > $@ 211 | 212 | pqiv.desktop: $(HEADERS) 213 | $(SILENT_GEN) ( \ 214 | echo "[Desktop Entry]"; \ 215 | echo "Version=1.0"; \ 216 | echo "Type=Application"; \ 217 | echo "Comment=Powerful quick image viewer"; \ 218 | echo "Name=pqiv"; \ 219 | echo "NoDisplay=true"; \ 220 | echo "Icon=emblem-photos"; \ 221 | echo "TryExec=$(PREFIX)/bin/pqiv"; \ 222 | echo "Exec=$(PREFIX)/bin/pqiv %F"; \ 223 | echo "MimeType=$(shell cat $(foreach BACKEND, $(sort $(BACKENDS)), $(SOURCEDIR)backends/$(BACKEND).mime) /dev/null | sort | uniq | awk 'ORS=";"')"; \ 224 | echo "Categories=Graphics;"; \ 225 | echo "Keywords=Viewer;" \ 226 | ) > $@ 227 | 228 | install: all 229 | mkdir -p $(DESTDIR)$(BINDIR) 230 | install pqiv$(EXECUTABLE_EXTENSION) $(DESTDIR)$(BINDIR)/pqiv$(EXECUTABLE_EXTENSION) 231 | -mkdir -p $(DESTDIR)$(MANDIR)/man1 232 | -install -m 644 $(SOURCEDIR)pqiv.1 $(DESTDIR)$(MANDIR)/man1/pqiv.1 233 | -mkdir -p $(DESTDIR)$(PREFIX)/share/applications 234 | -install -m 644 pqiv.desktop $(DESTDIR)$(PREFIX)/share/applications/pqiv.desktop 235 | ifeq ($(BACKENDS_BUILD), shared) 236 | mkdir -p $(DESTDIR)$(LIBDIR)/pqiv 237 | install $(SHARED_OBJECTS) $(DESTDIR)$(LIBDIR)/pqiv/ 238 | endif 239 | 240 | uninstall: 241 | rm -f $(DESTDIR)$(PREFIX)/bin/pqiv$(EXECUTABLE_EXTENSION) 242 | rm -f $(DESTDIR)$(MANDIR)/man1/pqiv.1 243 | rm -f $(DESTDIR)$(PREFIX)/share/applications/pqiv.desktop 244 | ifeq ($(BACKENDS_BUILD), shared) 245 | rm -f $(foreach SO_FILE, $(SHARED_OBJECTS), $(DESTDIR)$(LIBDIR)/pqiv/$(notdir $(SO_FILE))) 246 | rmdir $(DESTDIR)$(LIBDIR)/pqiv 247 | endif 248 | 249 | # Rudimentary MacOS bundling 250 | # Only really useful for opening pqiv using "open pqiv.app --args ..." from the 251 | # command line right now, but that already has the benefit that the application 252 | # window will be visible right away 253 | pqiv.app: pqiv.app.tmp 254 | rm -f ../$@ 255 | cd pqiv.app.tmp && zip -9r ../$@ . 256 | 257 | pqiv.app.tmp: pqiv.app.tmp/Contents/MacOS/pqiv pqiv.app.tmp/Contents/Info.plist pqiv.app.tmp/Contents/PkgInfo 258 | 259 | pqiv.app.tmp/Contents/MacOS/pqiv: 260 | -mkdir -p pqiv.app.tmp/Contents/MacOS 261 | install pqiv$(EXECUTABLE_EXTENSION) $@ 262 | 263 | pqiv.app.tmp/Contents/PkgInfo: 264 | -mkdir -p pqiv.app.tmp/Contents 265 | $(SILENT_GEN) ( \ 266 | echo -n "APPL????"; \ 267 | ) > $@ 268 | 269 | pqiv.app.tmp/Contents/Info.plist: $(HEADERS) 270 | -mkdir -p pqiv.app.tmp/Contents 271 | $(SILENT_GEN) ( \ 272 | echo ''; \ 273 | echo 'CFBundleNamepqivCFBundleDisplayNamepqiv'; \ 274 | echo 'CFBundleIdentifiercom.pberndt.pqivCFBundleVersion$(PQIV_VERSION_STRING)'; \ 275 | echo 'CFBundlePackageTypeAPPLCFBundleExecutablepqivLSMinimumSystemVersion'; \ 276 | echo '10.4CFBundleDocumentTypesCFBundleTypeMIMETypes'; \ 277 | cat $(foreach BACKEND, $(sort $(BACKENDS)), $(SOURCEDIR)backends/$(BACKEND).mime) /dev/null | sort | uniq | awk '{print "" $$0 ""}'; \ 278 | echo ''; \ 279 | ) > $@ 280 | 281 | clean: 282 | rm -f pqiv$(EXECUTABLE_EXTENSION) *.o backends/*.o backends/*.so lib/*.o backends/initializer-*.c pqiv.desktop 283 | 284 | distclean: clean 285 | rm -f config.make 286 | 287 | get_libs: 288 | $(info LIBS: $(LIBS)) 289 | @true 290 | 291 | get_available_backends: 292 | @OUT=; $(foreach BACKEND_C, $(wildcard $(SOURCEDIR)backends/*.c), \ 293 | [ "$(DISABLE_AUTOMATED_BUILD_$(basename $(notdir $(BACKEND_C))))" != "yes" ] && \ 294 | [ -n "$(LIBS_$(basename $(notdir $(BACKEND_C))))" ] && \ 295 | $(PKG_CONFIG) --exists "$(LIBS_$(basename $(notdir $(BACKEND_C))))" \ 296 | && OUT="$$OUT $(basename $(notdir $(BACKEND_C))) ";) echo BACKENDS: $$OUT 297 | @true 298 | -------------------------------------------------------------------------------- /backends/gdkpixbuf.c: -------------------------------------------------------------------------------- 1 | /** 2 | * pqiv 3 | * 4 | * Copyright (c) 2013-2017, Phillip Berndt 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | * 18 | * gdk-pixbuf backend 19 | */ 20 | 21 | #include "../pqiv.h" 22 | #include 23 | 24 | /* Default (GdkPixbuf) file type implementation {{{ */ 25 | typedef struct { 26 | // The surface where the image is stored. Only non-NULL for 27 | // the current, previous and next image. 28 | cairo_surface_t *image_surface; 29 | 30 | // For file_type & FILE_FLAGS_ANIMATION, this stores the 31 | // whole animation. As with the surface, this is only non-NULL 32 | // for the current, previous and next image. 33 | GdkPixbufAnimation *pixbuf_animation; 34 | GdkPixbufAnimationIter *animation_iter; 35 | 36 | #if GLIB_CHECK_VERSION(2, 62, 0) 37 | /* Glib 2.62 marks GTimeVal deprecated, but GdkPixbuf does not have an equivalent API 38 | * for the replacement structure yet. */ 39 | G_GNUC_BEGIN_IGNORE_DEPRECATIONS 40 | #endif 41 | GTimeVal animation_time; 42 | #if GLIB_CHECK_VERSION(2, 62, 0) 43 | G_GNUC_END_IGNORE_DEPRECATIONS 44 | #endif 45 | } file_private_data_gdkpixbuf_t; 46 | 47 | BOSNode *file_type_gdkpixbuf_alloc(load_images_state_t state, file_t *file) {/*{{{*/ 48 | file->private = (void *)g_slice_new0(file_private_data_gdkpixbuf_t); 49 | return load_images_handle_parameter_add_file(state, file); 50 | }/*}}}*/ 51 | void file_type_gdkpixbuf_free(file_t *file) {/*{{{*/ 52 | g_slice_free(file_private_data_gdkpixbuf_t, file->private); 53 | }/*}}}*/ 54 | void file_type_gdkpixbuf_unload(file_t *file) {/*{{{*/ 55 | file_private_data_gdkpixbuf_t *private = file->private; 56 | if(private->pixbuf_animation != NULL) { 57 | g_object_unref(private->pixbuf_animation); 58 | private->pixbuf_animation = NULL; 59 | } 60 | if(private->image_surface != NULL) { 61 | cairo_surface_destroy(private->image_surface); 62 | private->image_surface = NULL; 63 | } 64 | if(private->animation_iter != NULL) { 65 | g_object_unref(private->animation_iter); 66 | private->animation_iter = NULL; 67 | } 68 | }/*}}}*/ 69 | double file_type_gdkpixbuf_animation_initialize(file_t *file) {/*{{{*/ 70 | file_private_data_gdkpixbuf_t *private = file->private; 71 | if(private->animation_iter == NULL) { 72 | private->animation_iter = gdk_pixbuf_animation_get_iter(private->pixbuf_animation, &private->animation_time); 73 | } 74 | return gdk_pixbuf_animation_iter_get_delay_time(private->animation_iter); 75 | }/*}}}*/ 76 | double file_type_gdkpixbuf_animation_next_frame(file_t *file) {/*{{{*/ 77 | file_private_data_gdkpixbuf_t *private = (file_private_data_gdkpixbuf_t *)file->private; 78 | 79 | cairo_surface_t *surface = cairo_surface_reference(private->image_surface); 80 | 81 | // We keep track of time manually to allow the user to adjust the playback speed: 82 | // It is assumed that this function is called exactly at the right time, each time. 83 | // TODO The downside from this is that animations won't play smoothly on slow X11 connections. 84 | // Maybe I should extend the API to allow to switch between auto and manual time? 85 | int millis_until_next = gdk_pixbuf_animation_iter_get_delay_time(private->animation_iter); 86 | if(millis_until_next > 0) { 87 | private->animation_time.tv_usec += millis_until_next * 1000; 88 | if(private->animation_time.tv_usec >= 1000000) { 89 | private->animation_time.tv_sec += private->animation_time.tv_usec / 1000000; 90 | private->animation_time.tv_usec %= 1000000; 91 | } 92 | } 93 | 94 | gdk_pixbuf_animation_iter_advance(private->animation_iter, &private->animation_time); 95 | GdkPixbuf *pixbuf = gdk_pixbuf_animation_iter_get_pixbuf(private->animation_iter); 96 | 97 | cairo_t *sf_cr = cairo_create(surface); 98 | cairo_save(sf_cr); 99 | cairo_set_source_rgba(sf_cr, 0., 0., 0., 0.); 100 | cairo_set_operator(sf_cr, CAIRO_OPERATOR_SOURCE); 101 | cairo_paint(sf_cr); 102 | cairo_restore(sf_cr); 103 | gdk_cairo_set_source_pixbuf(sf_cr, pixbuf, 0, 0); 104 | cairo_paint(sf_cr); 105 | cairo_destroy(sf_cr); 106 | 107 | cairo_surface_destroy(surface); 108 | 109 | return gdk_pixbuf_animation_iter_get_delay_time(private->animation_iter); 110 | }/*}}}*/ 111 | gboolean file_type_gdkpixbuf_load_destroy_old_image_callback(gpointer old_surface) {/*{{{*/ 112 | cairo_surface_destroy((cairo_surface_t *)old_surface); 113 | return FALSE; 114 | }/*}}}*/ 115 | void file_type_gdkpixbuf_load(file_t *file, GInputStream *data, GError **error_pointer) {/*{{{*/ 116 | file_private_data_gdkpixbuf_t *private = (file_private_data_gdkpixbuf_t *)file->private; 117 | GdkPixbufAnimation *pixbuf_animation = NULL; 118 | 119 | #if (GDK_PIXBUF_MAJOR > 2 || (GDK_PIXBUF_MAJOR == 2 && GDK_PIXBUF_MINOR >= 28)) 120 | pixbuf_animation = gdk_pixbuf_animation_new_from_stream(data, image_loader_cancellable, error_pointer); 121 | #else 122 | #define IMAGE_LOADER_BUFFER_SIZE (1024 * 512) 123 | 124 | GdkPixbufLoader *loader = gdk_pixbuf_loader_new(); 125 | guchar *buffer = g_malloc(IMAGE_LOADER_BUFFER_SIZE); 126 | while(TRUE) { 127 | gssize bytes_read = g_input_stream_read(data, buffer, IMAGE_LOADER_BUFFER_SIZE, image_loader_cancellable, error_pointer); 128 | if(bytes_read == 0) { 129 | // All OK, finish the image loader 130 | gdk_pixbuf_loader_close(loader, error_pointer); 131 | pixbuf_animation = gdk_pixbuf_loader_get_animation(loader); 132 | if(pixbuf_animation != NULL) { 133 | g_object_ref(pixbuf_animation); // see above 134 | } 135 | break; 136 | } 137 | if(bytes_read == -1) { 138 | // Error. Handle this below. 139 | gdk_pixbuf_loader_close(loader, NULL); 140 | break; 141 | } 142 | // In all other cases, write to image loader 143 | if(!gdk_pixbuf_loader_write(loader, buffer, bytes_read, error_pointer)) { 144 | // In case of an error, abort. 145 | break; 146 | } 147 | } 148 | g_free(buffer); 149 | g_object_unref(loader); 150 | #endif 151 | 152 | if(pixbuf_animation == NULL) { 153 | return; 154 | } 155 | 156 | if(!gdk_pixbuf_animation_is_static_image(pixbuf_animation)) { 157 | if(private->pixbuf_animation != NULL) { 158 | g_object_unref(private->pixbuf_animation); 159 | } 160 | private->pixbuf_animation = g_object_ref(pixbuf_animation); 161 | file->file_flags |= FILE_FLAGS_ANIMATION; 162 | } 163 | else { 164 | file->file_flags &= ~FILE_FLAGS_ANIMATION; 165 | } 166 | 167 | GdkPixbuf *pixbuf = g_object_ref(gdk_pixbuf_animation_get_static_image(pixbuf_animation)); 168 | g_object_unref(pixbuf_animation); 169 | 170 | if(pixbuf != NULL) { 171 | GdkPixbuf *new_pixbuf = gdk_pixbuf_apply_embedded_orientation(pixbuf); 172 | g_object_unref(pixbuf); 173 | pixbuf = new_pixbuf; 174 | 175 | // This should never happen and is only here as a security measure 176 | // (glib will abort() if malloc() fails and nothing else can happen here) 177 | if(pixbuf == NULL) { 178 | return; 179 | } 180 | 181 | file->width = gdk_pixbuf_get_width(pixbuf); 182 | file->height = gdk_pixbuf_get_height(pixbuf); 183 | 184 | // Cairo cannot handle files larger than 32767x32767 185 | // See https://lists.freedesktop.org/archives/cairo/2009-August/017881.html 186 | // But actually, we might have to use a lower limit in case we are out of memory. 187 | double cairo_image_dimensions_limit = 30000.; 188 | 189 | cairo_surface_t *surface = NULL; 190 | do { 191 | if(file->width > cairo_image_dimensions_limit || file->height > cairo_image_dimensions_limit) { 192 | double loading_scale_factor = 1.; 193 | loading_scale_factor = fmin(cairo_image_dimensions_limit / file->width, cairo_image_dimensions_limit / file->height); 194 | file->width *= loading_scale_factor; 195 | file->height *= loading_scale_factor; 196 | g_printerr("Warning: Resizing file %s down to %dx%d due to Cairo's image size limit / insufficient memory.\n", 197 | file->display_name, file->width, file->height); 198 | 199 | new_pixbuf = gdk_pixbuf_scale_simple(new_pixbuf, file->width, file->height, GDK_INTERP_BILINEAR); 200 | if(!new_pixbuf) { 201 | if(cairo_image_dimensions_limit > 10000) { 202 | cairo_image_dimensions_limit -= 10000; 203 | continue; 204 | } 205 | g_object_unref(pixbuf); 206 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-pixbuf-error"), 1, "Failed to allocate memory for the resized image.\n"); 207 | return; 208 | } 209 | else { 210 | g_object_unref(pixbuf); 211 | pixbuf = new_pixbuf; 212 | } 213 | } 214 | 215 | #if 0 && (GDK_MAJOR_VERSION == 3 && GDK_MINOR_VERSION >= 10) || (GDK_MAJOR_VERSION > 3) 216 | // This function has a bug, see 217 | // https://bugzilla.gnome.org/show_bug.cgi?id=736624 218 | // We therefore have to use the below version even if this function is available. 219 | surface = gdk_cairo_surface_create_from_pixbuf(pixbuf, 1., NULL); 220 | // TODO Once this works, manually check if surface failed with "out of memory". 221 | #else 222 | surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, file->width, file->height); 223 | if(cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) { 224 | g_object_unref(pixbuf); 225 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-pixbuf-error"), 1, "Failed to create a cairo image surface for the loaded image (cairo status %d)\n", cairo_surface_status(surface)); 226 | return; 227 | } 228 | cairo_t *sf_cr = cairo_create(surface); 229 | gdk_cairo_set_source_pixbuf(sf_cr, pixbuf, 0, 0); 230 | cairo_paint(sf_cr); 231 | if(cairo_status(sf_cr) == CAIRO_STATUS_NO_MEMORY) { 232 | // Failed due to out of memory - retry with smaller copy of the image 233 | cairo_destroy(sf_cr); 234 | cairo_surface_destroy(surface); 235 | if(cairo_image_dimensions_limit > 10000) { 236 | cairo_image_dimensions_limit -= 10000; 237 | continue; 238 | } 239 | g_object_unref(pixbuf); 240 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-pixbuf-error"), 1, "Insufficient memory to load image"); 241 | return; 242 | } 243 | cairo_destroy(sf_cr); 244 | #endif 245 | 246 | break; 247 | } 248 | while(TRUE); // Do not ever repeat, only on explicit "continue", see break just above. 249 | 250 | cairo_surface_t *old_surface = private->image_surface; 251 | private->image_surface = surface; 252 | if(old_surface != NULL) { 253 | g_idle_add(file_type_gdkpixbuf_load_destroy_old_image_callback, old_surface); 254 | } 255 | g_object_unref(pixbuf); 256 | 257 | file->is_loaded = TRUE; 258 | } 259 | }/*}}}*/ 260 | void file_type_gdkpixbuf_draw(file_t *file, cairo_t *cr) {/*{{{*/ 261 | file_private_data_gdkpixbuf_t *private = (file_private_data_gdkpixbuf_t *)file->private; 262 | 263 | cairo_surface_t *current_image_surface = private->image_surface; 264 | cairo_set_source_surface(cr, current_image_surface, 0, 0); 265 | apply_interpolation_quality(cr); 266 | cairo_paint(cr); 267 | }/*}}}*/ 268 | 269 | void file_type_gdkpixbuf_initializer(file_type_handler_t *info) {/*{{{*/ 270 | // Fill the file filter pattern 271 | info->file_types_handled = gtk_file_filter_new(); 272 | gtk_file_filter_add_pixbuf_formats(info->file_types_handled); 273 | GSList *file_formats_list = gdk_pixbuf_get_formats(); 274 | for(GSList *file_formats_iterator = file_formats_list; file_formats_iterator; file_formats_iterator = g_slist_next(file_formats_iterator)) { 275 | gchar **file_format_extensions_iterator = gdk_pixbuf_format_get_extensions(file_formats_iterator->data); 276 | while(*file_format_extensions_iterator != NULL) { 277 | gchar *extn = g_strdup_printf("*.%s", *file_format_extensions_iterator); 278 | gtk_file_filter_add_pattern(info->file_types_handled, extn); 279 | g_free(extn); 280 | ++file_format_extensions_iterator; 281 | } 282 | }; 283 | g_slist_free(file_formats_list); 284 | 285 | // Assign the handlers 286 | info->alloc_fn = file_type_gdkpixbuf_alloc; 287 | info->free_fn = file_type_gdkpixbuf_free; 288 | info->load_fn = file_type_gdkpixbuf_load; 289 | info->unload_fn = file_type_gdkpixbuf_unload; 290 | info->animation_initialize_fn = file_type_gdkpixbuf_animation_initialize; 291 | info->animation_next_frame_fn = file_type_gdkpixbuf_animation_next_frame; 292 | info->draw_fn = file_type_gdkpixbuf_draw; 293 | }/*}}}*/ 294 | /* }}} */ 295 | -------------------------------------------------------------------------------- /backends/wand.c: -------------------------------------------------------------------------------- 1 | /** 2 | * pqiv 3 | * 4 | * Copyright (c) 2013-2017, Phillip Berndt 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | * 18 | * ImageMagick wand backend 19 | * 20 | */ 21 | 22 | #include "../pqiv.h" 23 | #include "../lib/filebuffer.h" 24 | #include 25 | #include 26 | #include 27 | 28 | 29 | #if __clang__ 30 | // ImageMagick does throw some clang warnings 31 | #pragma clang diagnostic push 32 | #pragma clang diagnostic ignored "-Wunused-variable" 33 | #pragma clang diagnostic ignored "-Wunknown-attributes" 34 | #pragma clang diagnostic ignored "-Wkeyword-macro" 35 | #endif 36 | 37 | #if defined(WAND_VERSION) && WAND_VERSION > 6 38 | #include 39 | #else 40 | #include 41 | #endif 42 | 43 | #if __clang__ 44 | #pragma clang diagnostic pop 45 | #endif 46 | 47 | #include 48 | 49 | // ImageMagick's multithreading is broken. To test this, open a multi-page 50 | // postscript document using this backend without --low-memory and then quit 51 | // pqiv. The backend will freeze in MagickWandTerminus() while waiting for 52 | // a Mutex. We must do this call to allow ImageMagick to delete temporary 53 | // files created using postscript processing (in /tmp usually). 54 | // 55 | // The only way around this, sadly, is to use a global mutex around all 56 | // ImageMagick calls. 57 | G_LOCK_DEFINE_STATIC(magick_wand_global_lock); 58 | 59 | typedef struct { 60 | MagickWand *wand; 61 | cairo_surface_t *rendered_image_surface; 62 | 63 | // Starting from 1 for numbered files, 0 for unpaginated files 64 | unsigned int page_number; 65 | } file_private_data_wand_t; 66 | 67 | // Check if a (named) file has a certain extension. Used for psd fix and multi-page detection (ps, pdf, ..) 68 | static gboolean file_type_wand_has_extension(file_t *file, const char *extension) { 69 | char *actual_extension; 70 | return (!(file->file_flags & FILE_FLAGS_MEMORY_IMAGE) && file->file_name && (actual_extension = strrchr(file->file_name, '.')) && strcasecmp(actual_extension, extension) == 0); 71 | } 72 | 73 | // Functions to render the Magick backend to a cairo surface via in-memory PNG export 74 | cairo_status_t file_type_wand_read_data(void *closure, unsigned char *data, unsigned int length) {/*{{{*/ 75 | unsigned char **pos = closure; 76 | memcpy(data, *pos, length); 77 | *pos += length; 78 | return CAIRO_STATUS_SUCCESS; 79 | }/*}}}*/ 80 | void file_type_wand_update_image_surface(file_t *file) {/*{{{*/ 81 | file_private_data_wand_t *private = file->private; 82 | 83 | if(private->rendered_image_surface) { 84 | cairo_surface_destroy(private->rendered_image_surface); 85 | private->rendered_image_surface = NULL; 86 | } 87 | 88 | MagickSetImageFormat(private->wand, "PNG32"); 89 | 90 | size_t image_size; 91 | unsigned char *image_data = MagickGetImageBlob(private->wand, &image_size); 92 | unsigned char *image_data_loc = image_data; 93 | 94 | private->rendered_image_surface = cairo_image_surface_create_from_png_stream(file_type_wand_read_data, &image_data_loc); 95 | 96 | MagickRelinquishMemory(image_data); 97 | }/*}}}*/ 98 | 99 | BOSNode *file_type_wand_alloc(load_images_state_t state, file_t *file) {/*{{{*/ 100 | G_LOCK(magick_wand_global_lock); 101 | 102 | if(file_type_wand_has_extension(file, ".pdf") || file_type_wand_has_extension(file, ".ps")) { 103 | // Multi-page document. Load number of pages and create one file_t per page 104 | GError *error_pointer = NULL; 105 | MagickWand *wand = NewMagickWand(); 106 | GBytes *image_bytes = buffered_file_as_bytes(file, NULL, &error_pointer); 107 | if(!image_bytes) { 108 | g_printerr("Failed to read image %s: %s\n", file->file_name, error_pointer->message); 109 | g_clear_error(&error_pointer); 110 | G_UNLOCK(magick_wand_global_lock); 111 | file_free(file); 112 | return FALSE_POINTER; 113 | } 114 | size_t image_size; 115 | const gchar *image_data = g_bytes_get_data(image_bytes, &image_size); 116 | MagickBooleanType success = MagickReadImageBlob(wand, image_data, image_size); 117 | if(success == MagickFalse) { 118 | ExceptionType severity; 119 | char *message = MagickGetException(wand, &severity); 120 | g_printerr("Failed to read image %s: %s\n", file->file_name, message); 121 | MagickRelinquishMemory(message); 122 | DestroyMagickWand(wand); 123 | buffered_file_unref(file); 124 | G_UNLOCK(magick_wand_global_lock); 125 | file_free(file); 126 | return FALSE_POINTER; 127 | } 128 | 129 | int n_pages = MagickGetNumberImages(wand); 130 | DestroyMagickWand(wand); 131 | buffered_file_unref(file); 132 | 133 | BOSNode *first_node = FALSE_POINTER; 134 | for(int n=0; ndisplay_name, n + 1), 138 | g_strdup_printf("%s[%d]", file->sort_name, n + 1)); 139 | new_file->private = g_slice_new0(file_private_data_wand_t); 140 | ((file_private_data_wand_t *)new_file->private)->page_number = n + 1; 141 | 142 | // Temporarily give up lock to do this: Otherwise we might see a deadlock 143 | // if another thread holding the file tree's lock is waiting for the wand 144 | // lock for another operation. 145 | G_UNLOCK(magick_wand_global_lock); 146 | if(n == 0) { 147 | first_node = load_images_handle_parameter_add_file(state, new_file); 148 | } 149 | else { 150 | load_images_handle_parameter_add_file(state, new_file); 151 | } 152 | G_LOCK(magick_wand_global_lock); 153 | } 154 | 155 | if(first_node) { 156 | file_free(file); 157 | } 158 | G_UNLOCK(magick_wand_global_lock); 159 | return first_node; 160 | } 161 | else { 162 | // Simple image 163 | file->private = g_slice_new0(file_private_data_wand_t); 164 | BOSNode *first_node = load_images_handle_parameter_add_file(state, file); 165 | G_UNLOCK(magick_wand_global_lock); 166 | return first_node; 167 | } 168 | }/*}}}*/ 169 | void file_type_wand_free(file_t *file) {/*{{{*/ 170 | g_slice_free(file_private_data_wand_t, file->private); 171 | }/*}}}*/ 172 | void file_type_wand_load(file_t *file, GInputStream *data, GError **error_pointer) {/*{{{*/ 173 | G_LOCK(magick_wand_global_lock); 174 | file_private_data_wand_t *private = file->private; 175 | 176 | private->wand = NewMagickWand(); 177 | gsize image_size; 178 | GBytes *image_bytes = buffered_file_as_bytes(file, data, error_pointer); 179 | if(!image_bytes) { 180 | G_UNLOCK(magick_wand_global_lock); 181 | return; 182 | } 183 | const gchar *image_data = g_bytes_get_data(image_bytes, &image_size); 184 | MagickBooleanType success = MagickReadImageBlob(private->wand, image_data, image_size); 185 | 186 | if(success == MagickFalse) { 187 | ExceptionType severity; 188 | char *message = MagickGetException(private->wand, &severity); 189 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-wand-error"), 1, "Failed to load image %s: %s", file->file_name, message); 190 | MagickRelinquishMemory(message); 191 | DestroyMagickWand(private->wand); 192 | private->wand = NULL; 193 | buffered_file_unref(file); 194 | G_UNLOCK(magick_wand_global_lock); 195 | return; 196 | } 197 | 198 | MagickResetIterator(private->wand); 199 | if(private->page_number > 0) { 200 | // PDF/PS files are displayed one page per file_t 201 | MagickSetIteratorIndex(private->wand, private->page_number - 1); 202 | } 203 | else { 204 | // Other files are either interpreted as animated (if they have a delay 205 | // set) or merged down to one image (interpreted as layered, as in 206 | // PSD/XCF files) 207 | size_t delay = MagickGetImageDelay(private->wand); 208 | if(delay) { 209 | MagickWand *wand = MagickCoalesceImages(private->wand); 210 | DestroyMagickWand(private->wand); 211 | private->wand = wand; 212 | MagickResetIterator(wand); 213 | 214 | file->file_flags |= FILE_FLAGS_ANIMATION; 215 | } 216 | else if(MagickGetNumberImages(private->wand) > 1) { 217 | // Merge multi-page files. 218 | // This doesn't work as expected for .psd files. As a hack, disable 219 | // it for them. 220 | // TODO Check periodically if the problem still persists (heavily distorted images) and remove this once it has been solved 221 | if(!file_type_wand_has_extension(file, ".psd")) { 222 | MagickWand *wand = MagickMergeImageLayers(private->wand, FlattenLayer); 223 | DestroyMagickWand(private->wand); 224 | private->wand = wand; 225 | MagickResetIterator(private->wand); 226 | } 227 | } 228 | MagickNextImage(private->wand); 229 | } 230 | file_type_wand_update_image_surface(file); 231 | 232 | file->width = MagickGetImageWidth(private->wand); 233 | file->height = MagickGetImageHeight(private->wand); 234 | file->is_loaded = TRUE; 235 | G_UNLOCK(magick_wand_global_lock); 236 | }/*}}}*/ 237 | double file_type_wand_animation_initialize(file_t *file) {/*{{{*/ 238 | file_private_data_wand_t *private = file->private; 239 | // The unit of MagickGetImageDelay is "ticks-per-second" 240 | return 1000. / MagickGetImageDelay(private->wand); 241 | }/*}}}*/ 242 | double file_type_wand_animation_next_frame(file_t *file) {/*{{{*/ 243 | // ImageMagick tends to be really slow when it comes to loading frames. 244 | // We therefore measure the required time and subtract it from the time 245 | // pqiv waits before loading the next frame: 246 | G_LOCK(magick_wand_global_lock); 247 | gint64 begin_time = g_get_monotonic_time(); 248 | 249 | file_private_data_wand_t *private = file->private; 250 | 251 | MagickBooleanType status = MagickNextImage(private->wand); 252 | if(status == MagickFalse) { 253 | MagickResetIterator(private->wand); 254 | MagickNextImage(private->wand); 255 | } 256 | file_type_wand_update_image_surface(file); 257 | 258 | gint64 required_time = (g_get_monotonic_time() - begin_time) / 1000; 259 | gint pause = 1000. / MagickGetImageDelay(private->wand); 260 | 261 | G_UNLOCK(magick_wand_global_lock); 262 | 263 | return pause + 1 > required_time ? pause - required_time : 1; 264 | }/*}}}*/ 265 | void file_type_wand_unload(file_t *file) {/*{{{*/ 266 | G_LOCK(magick_wand_global_lock); 267 | file_private_data_wand_t *private = file->private; 268 | 269 | if(private->rendered_image_surface) { 270 | cairo_surface_destroy(private->rendered_image_surface); 271 | private->rendered_image_surface = NULL; 272 | } 273 | 274 | if(private->wand) { 275 | DestroyMagickWand(private->wand); 276 | private->wand = NULL; 277 | 278 | buffered_file_unref(file); 279 | } 280 | G_UNLOCK(magick_wand_global_lock); 281 | }/*}}}*/ 282 | void file_type_wand_draw(file_t *file, cairo_t *cr) {/*{{{*/ 283 | file_private_data_wand_t *private = file->private; 284 | 285 | if(private->rendered_image_surface) { 286 | if(private->page_number > 0) { 287 | // Is multi-page document. Draw white background. 288 | cairo_set_source_rgb(cr, 1., 1., 1.); 289 | cairo_paint(cr); 290 | cairo_set_operator(cr, CAIRO_OPERATOR_OVER); 291 | } 292 | cairo_set_source_surface(cr, private->rendered_image_surface, 0, 0); 293 | apply_interpolation_quality(cr); 294 | cairo_paint(cr); 295 | } 296 | }/*}}}*/ 297 | 298 | static void file_type_wand_exit_handler() {/*{{{*/ 299 | G_LOCK(magick_wand_global_lock); 300 | MagickWandTerminus(); 301 | G_UNLOCK(magick_wand_global_lock); 302 | }/*}}}*/ 303 | 304 | void file_type_wand_initializer(file_type_handler_t *info) {/*{{{*/ 305 | // Fill the file filter pattern 306 | MagickWandGenesis(); 307 | info->file_types_handled = gtk_file_filter_new(); 308 | size_t count, i; 309 | char **formats = MagickQueryFormats("*", &count); 310 | for(i=0; ifile_types_handled, format); 334 | g_free(format); 335 | } 336 | MagickRelinquishMemory(formats); 337 | 338 | // We need to register MagickWandTerminus(), imageMagick's exit handler, to 339 | // cleanup temporary files when pqiv exits. 340 | atexit(file_type_wand_exit_handler); 341 | 342 | // Magick Wand does not give us MIME types. Manually add the most interesting one: 343 | gtk_file_filter_add_mime_type(info->file_types_handled, "image/vnd.adobe.photoshop"); 344 | 345 | // Assign the handlers 346 | info->alloc_fn = file_type_wand_alloc; 347 | info->free_fn = file_type_wand_free; 348 | info->load_fn = file_type_wand_load; 349 | info->unload_fn = file_type_wand_unload; 350 | info->draw_fn = file_type_wand_draw; 351 | info->animation_initialize_fn = file_type_wand_animation_initialize; 352 | info->animation_next_frame_fn = file_type_wand_animation_next_frame; 353 | }/*}}}*/ 354 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Configure script for pqiv. Running this script is optional if you only want 4 | # gdk-pixbuf support, but still recommended. 5 | # 6 | 7 | tempdir() { 8 | NAME=tmp_${RANDOM} 9 | while ! mkdir "${NAME}" 2>/dev/null; do 10 | NAME=tmp_${RANDOM} 11 | done 12 | echo ${NAME} 13 | } 14 | 15 | PREFIX=/usr 16 | DESTDIR= 17 | GTK_VERSION=0 18 | CROSS= 19 | BINARY_EXTENSION= 20 | BINDIR= 21 | LIBDIR= 22 | MANDIR= 23 | BACKENDS= 24 | ENFORCED_BACKENDS= 25 | DISABLED_BACKENDS= 26 | EXTRA_DEFS= 27 | BACKENDS_BUILD=static 28 | DEBUG_DEFS= 29 | 30 | # Check if configure was called from another directory; 31 | # support for out-of-source-tree builds 32 | STARTUP_DIR="$(pwd)" 33 | SOURCE_DIR="$( (cd "$(dirname "$0")"; pwd -P) )" 34 | cd "$SOURCE_DIR" 35 | 36 | # For development, you can set default settings here 37 | [ -x ./configure-dev ] && . ./configure-dev 38 | 39 | # Help and options 40 | help() { 41 | cat >&2 < Alternative syntax for backend selection. Non-specified backends are 67 | autodetermined. 68 | --backends-build=.. Either \`shared' to compile the backends as shared libraries, 69 | or \`static' to compile them into pqiv. \`shared' is only of use if you 70 | plan to package pqiv and want to get rid of the run-time dependencies, 71 | so this defaults to \`static'. 72 | 73 | options to remove features from pqiv: 74 | EOF 75 | 76 | awk 'BEGIN { FS="ifndef|ifdef|option|:|/\\*|\\*/" } /ifn?def .+ option / { print $2 " " $4 " " $5 }' pqiv.c | while read DEFNAME OPTFLAG DESCRIPTION; do 77 | if [ ${#DESCRIPTION} -gt 50 ]; then 78 | printf " %-22s\n %-22s%s\n" $OPTFLAG "" "$DESCRIPTION" >&2 79 | else 80 | printf " %-22s%s\n" $OPTFLAG "$DESCRIPTION" >&2 81 | fi 82 | done 83 | echo >&2 84 | } 85 | 86 | while [ $# -gt 0 ]; do 87 | PARAMETER=${1%=*} 88 | VALUE=${1#*=} 89 | 90 | case $PARAMETER in 91 | --prefix) 92 | PREFIX=$VALUE 93 | ;; 94 | --destdir) 95 | DESTDIR=$VALUE 96 | ;; 97 | --libdir) 98 | LIBDIR=${VALUE} 99 | LIBDIR="${LIBDIR//\{/(}" 100 | LIBDIR="${LIBDIR//\}/)}" 101 | LIBDIR="${LIBDIR//prefix/PREFIX}" 102 | ;; 103 | --gtk-version) 104 | GTK_VERSION=$VALUE 105 | ;; 106 | -h) 107 | help 108 | exit 0 109 | ;; 110 | --help) 111 | help 112 | exit 0 113 | ;; 114 | --cross) 115 | CROSS=$VALUE 116 | if [ "${CROSS: -1}" != "-" ]; then 117 | CROSS="$CROSS-" 118 | fi 119 | ;; 120 | --backends-build) 121 | BACKENDS_BUILD=$VALUE 122 | if [ "$BACKENDS_BUILD" != "static" -a "$BACKENDS_BUILD" != "shared" ]; then 123 | echo "Invalid argument to --backends-build: Value must be either \`static' or \`shared'." >&2 124 | exit 1 125 | fi 126 | ;; 127 | --backends) 128 | BACKENDS="${VALUE//,/ }" 129 | for NAME in ${BACKENDS}; do 130 | [ -e backends/${NAME}.c ] && continue 131 | echo "Invalid argument to --backends: Backend ${NAME} was not found" >&2 132 | exit 1 133 | done 134 | ;; 135 | # Undocumented options for autoconf (esp. dh_auto_configure) 136 | # compatibility Note to maintainers: These options are here to make it 137 | # simpler to package pqiv, because they allow to run autotools wrappers 138 | # against this package. I will maintain them, but I'd recommend 139 | # against using them if you can avoid it. 140 | --host) 141 | CROSS=${VALUE}- 142 | ;; 143 | --bindir) 144 | BINDIR=${VALUE} 145 | BINDIR="${BINDIR//\{/(}" 146 | BINDIR="${BINDIR//\}/)}" 147 | BINDIR="${BINDIR//prefix/PREFIX}" 148 | echo "Use of autoconf option --bindir is discouraged, because support is incomplete. Rewrote \`$VALUE' to \`$BINDIR' and used that as the BINDIR Make variable." >&2 149 | ;; 150 | --mandir) 151 | MANDIR=${VALUE} 152 | MANDIR="${MANDIR//\{/(}" 153 | MANDIR="${MANDIR//\}/)}" 154 | MANDIR="${MANDIR//prefix/PREFIX}" 155 | echo "Use of autoconf option --mandir is discouraged, because support is incomplete. Rewrote \`$VALUE' to \`$MANDIR' and used that as the MANDIR Make variable." >&2 156 | ;; 157 | --disable-silent-rules) 158 | VERBOSE=1 159 | ;; 160 | --infodir | --sysconfdir | --includedir | --localstatedir | --libexecdir | --disable-maintainer-mode | --disable-dependency-tracking | --build | --sbindir | --includedir | --oldincludedir | --localedir | --docdir) 161 | echo "autoconf option ${PARAMETER} ignored" >&2 162 | ;; 163 | --no-sorting | --no-compositing | --no-fading | --no-commands | --no-config-file | --no-inotify | --no-animations | --binary-name) 164 | echo "obsolete 1.0 option ${PARAMETER} ignored" >&2 165 | ;; 166 | *) 167 | # Check for disableable feature flag 168 | DEF=$( 169 | awk 'BEGIN { FS="ifndef|ifdef|option|:|/\\*|\\*/" } /ifn?def .+ option / { print $2 " " $4 " " $5 }' pqiv.c | while read DEFNAME OPTFLAG DESCRIPTION; do 170 | if [ "${PARAMETER}" = "${OPTFLAG}" ]; then 171 | echo "-D${DEFNAME}" 172 | fi 173 | done 174 | ) 175 | if [ -n "${DEF}" ]; then 176 | EXTRA_DEFS="${EXTRA_DEFS} ${DEF}" 177 | else 178 | # Check for dynamic backend en-/dis-abling flag 179 | if [ "${PARAMETER#--without-}" != "${PARAMETER}" ]; then 180 | NAME="${PARAMETER#--without-}" 181 | if ! [ -e backends/${NAME}.c ]; then 182 | echo "Unknown option: $1" >&2 183 | exit 1 184 | fi 185 | DISABLED_BACKENDS="${PARAMETER#--without-} ${DISABLED_BACKENDS}" 186 | elif [ "${PARAMETER#--with-}" != "${PARAMETER}" ]; then 187 | NAME="${PARAMETER#--with-}" 188 | if ! [ -e backends/${NAME}.c ]; then 189 | echo "Unknown option: $1" >&2 190 | exit 1 191 | fi 192 | ENFORCED_BACKENDS="${NAME} ${ENFORCED_BACKENDS}" 193 | else 194 | echo "Unknown option: $1" >&2 195 | help 196 | exit 1 197 | fi 198 | fi 199 | esac 200 | shift 201 | done 202 | 203 | # The makefile is for GNU make 204 | if [ -z $MAKE ]; then 205 | MAKE=make 206 | if ! (${MAKE} -v 2>&1 | grep -q "GNU Make"); then 207 | MAKE=gmake 208 | if ! which $MAKE 2>&1 >/dev/null; then 209 | echo "GNU make is required for building pqiv" >&2 210 | exit 1 211 | fi 212 | fi 213 | fi 214 | 215 | # If cross-compiling, check if cc is present (usually it is not) 216 | if [ -n "$CROSS" -a -z "$CC" ]; then 217 | echo -n "Checking for cross-compiler cc.. " 218 | if ! which ${CROSS}cc >/dev/null 2>&1; then 219 | if which ${CROSS}clang >/dev/null 2>&1; then 220 | export CC=clang 221 | elif which ${CROSS}gcc >/dev/null 2>&1; then 222 | export CC=gcc 223 | else 224 | echo 225 | echo 226 | echo "No compiler found. Please set the appropriate CC environment variable." >&2 227 | exit 1 228 | fi 229 | echo "using ${CROSS}${CC}" 230 | else 231 | echo "ok" 232 | fi 233 | fi 234 | 235 | # Determine binary extension (for Windows) 236 | echo -n "Determining executable extension.. " 237 | DIR=`tempdir` 238 | cd $DIR 239 | echo 'int main(int argc, char *argv[]) { return 0; }' > test.c 240 | ${CROSS}${CC:-cc} ${CFLAGS} test.c ${LDFLAGS} 241 | RV=$? 242 | rm -f test.c 243 | EXECUTABLE=`ls` 244 | rm -f $EXECUTABLE 245 | cd .. 246 | rmdir $DIR 247 | if [ "$RV" != 0 ]; then 248 | echo 249 | echo 250 | echo "The compiler can't compile executables!?" >&2 251 | exit 1 252 | fi 253 | EXECUTABLE_EXTENSION=${EXECUTABLE#a} 254 | if [ "$EXECUTABLE_EXTENSION" = ".out" ]; then 255 | EXECUTABLE_EXTENSION= 256 | fi 257 | echo ${EXECUTABLE_EXTENSION:-(none)} 258 | 259 | # Do a rudimental prerequisites check to have user-friendlier error messages 260 | if [ -n "${CROSS}" -a -z "${PKG_CONFIG}" ]; then 261 | echo -n "Checking for pkg-config.. " 262 | PKG_CONFIG=${CROSS}pkg-config 263 | if ! which ${PKG_CONFIG} >/dev/null 2>&1; then 264 | echo 265 | echo 266 | echo "Did not find a specialized tool ${CROSS}pkg-config, defaulting to pkg-config" >&2 267 | echo "If you really ARE cross-compiling, the build might therefore fail!" >&2 268 | echo 269 | PKG_CONFIG=pkg-config 270 | else 271 | echo "${PKG_CONFIG}" 272 | fi 273 | fi 274 | 275 | # Auto-determine available backends 276 | if [ -z "$BACKENDS" ]; then 277 | echo -n "Checking for supported backends.. " 278 | BACKENDS="$($MAKE get_available_backends ${PKG_CONFIG:+PKG_CONFIG="$PKG_CONFIG"} | awk '/^BACKENDS:/ {print substr($0, 11);}')" 279 | echo "${BACKENDS:-(none)}" 280 | fi 281 | 282 | # Disable explicitly disabled and enable explicitly enabled backends 283 | for BACKEND in ${DISABLED_BACKENDS}; do 284 | BACKENDS="${BACKENDS//${BACKEND} /}" 285 | done 286 | for BACKEND in ${ENFORCED_BACKENDS}; do 287 | BACKENDS="${BACKEND} ${BACKENDS//${BACKEND} /}" 288 | done 289 | 290 | echo "Building with backends: ${BACKENDS:-(none)}" 291 | if [ -z "$BACKENDS" ]; then 292 | echo "WARNING: Building without backends! You won't be able to see _any_ images." >&2 293 | fi 294 | 295 | echo -n "Checking if the prerequisites are installed.. " 296 | LIBS_GTK3="`$MAKE get_libs GTK_VERSION=3 EXECUTABLE_EXTENSION=$EXECUTABLE_EXTENSION ${PKG_CONFIG:+PKG_CONFIG="$PKG_CONFIG"} ${BACKENDS:+BACKENDS="$BACKENDS"} | awk '/^LIBS:/ {print substr($0, 7);}'`" 297 | LIBS_GTK2="`$MAKE get_libs GTK_VERSION=2 EXECUTABLE_EXTENSION=$EXECUTABLE_EXTENSION ${PKG_CONFIG:+PKG_CONFIG="$PKG_CONFIG"} ${BACKENDS:+BACKENDS="$BACKENDS"} | awk '/^LIBS:/ {print substr($0, 7);}'`" 298 | 299 | if [ $? != 0 ]; then 300 | echo "failed." 301 | echo 302 | echo 303 | echo "Failed to run make. Is your make command compatible to GNU make?" >&2 304 | exit 1 305 | fi 306 | 307 | if ${PKG_CONFIG:-pkg-config} --exists "$LIBS_GTK3"; then 308 | LIBS="${LIBS_GTK3}" 309 | echo "ok" 310 | else 311 | if ${PKG_CONFIG:-pkg-config} --exists "$LIBS_GTK2"; then 312 | if [ "$GTK_VERSION" = 3 ]; then 313 | echo "failed." 314 | echo 315 | echo 316 | echo "GTK 2 was found, but you manually specified --gtk-version=3, which was not found." >&2 317 | echo "If you want GTK3, install the development packages for" >&2 318 | echo " ${LIBS_GTK3}" >&2 319 | exit 1 320 | fi 321 | echo "ok, found GTK 2" 322 | GTK_VERSION=2 323 | LIBS="${LIBS_GTK2}" 324 | else 325 | echo "failed." 326 | echo 327 | echo 328 | echo "Please install either the development packages for " >&2 329 | echo " ${LIBS_GTK3}" >&2 330 | echo "or for" >&2 331 | echo " ${LIBS_GTK2}" >&2 332 | exit 1 333 | fi 334 | fi 335 | 336 | if [ "$SOURCE_DIR" != "$STARTUP_DIR" ]; then 337 | if ! [ -e $STARTUP_DIR/GNUmakefile ]; then 338 | echo "Writing GNUmakefile." 339 | echo "include $SOURCE_DIR/GNUmakefile" > $STARTUP_DIR/GNUmakefile 340 | else 341 | echo "Not touching existing GNUmakefile." 342 | fi 343 | fi 344 | 345 | 346 | echo "Writing config.make." 347 | cat > $STARTUP_DIR/config.make </dev/null)" ]; then 375 | echo -n "Checking if affected by liblcms bug.. " 376 | DIR=`tempdir` 377 | cd $DIR 378 | echo -e "#include \n#include \nint main() { poppler_get_version(); spectre_status_to_string(SPECTRE_STATUS_SUCCESS); }" > test.c 379 | ${CROSS}${CC:-cc} ${CFLAGS} test.c ${LDFLAGS} $(${PKG_CONFIG:-pkg-config} --libs --cflags ${LIBS}) -o test >/dev/null 2>&1 380 | if [ -e test ]; then 381 | if [ "$(ldd test | grep -E "liblcms[0-9]?.so" | wc -l)" -gt 1 ]; then 382 | echo "yes" 383 | echo 2>&1 384 | echo "WARNING: You enabled both the spectre and poppler backends, and chose static linking." >&2 385 | echo "Your system uses two different versions of liblcms that are known to interfere:" 2>&1 386 | ldd test | grep -E "liblcms[0-9]?.so" >&2 387 | echo "Recompile using --backends-build=shared if you experience problems." 2>&1 388 | echo 2>&1 389 | else 390 | echo "no" 391 | fi 392 | else 393 | echo "test failed" 394 | fi 395 | rm -f test.c test 396 | cd .. 397 | rmdir $DIR 398 | fi 399 | 400 | echo 401 | echo "Done. Run \`$MAKE install' to install pqiv." 402 | exit 0 403 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | PQIV README 2 | =========== 3 | 4 | About pqiv 5 | ---------- 6 | 7 | pqiv is a powerful GTK 3 based command-line image viewer with a minimal UI. It 8 | is highly customizable, can be fully controlled from scripts, and has support 9 | for various file formats including PDF, Postscript, video files and archives. 10 | It is optimized to be quick and responsive. 11 | 12 | It comes with support for animations, slideshows, transparency, VIM-like key 13 | bindings, automated loading of new images as they appear, external image 14 | filters, marks, image preloading, and much more. 15 | 16 | pqiv started as a Python rewrite of qiv avoiding imlib, but evolved into a much 17 | more powerful tool. Today, pqiv stands for powerful quick image viewer. 18 | 19 | Features 20 | -------- 21 | 22 | * Recursive loading from directories 23 | * Can watch files and directories for changes 24 | * Sorts images in natural order 25 | * Has a status bar showing information on the current image 26 | * Comes with transparency support 27 | * Can move/zoom/rotate/flip images 28 | * Can pipe images through external filters 29 | * Loads the next image in the background for quick response times 30 | * Caches zoomed images for smoother movement 31 | * Supports fade image transition animations 32 | * Supports various image and video formats through a rich set of backends 33 | * Comes with an interactive montage mode (a.k.a. "image grid") 34 | * Customizable key-bindings with support for VIM-like key sequences, action 35 | cycling and binding multiple actions to a single key 36 | * Mark/unmark images and pipe the list of marked images to an external script 37 | 38 | Installation 39 | ------------ 40 | 41 | Usual stuff. `./configure && make && make install`. The configure script is 42 | optional if you only want gdk-pixbuf support and will auto-determine which 43 | backends to build if invoked without parameters. 44 | 45 | You can also use precompiled and packaged versions of pqiv. Note that the 46 | distribution packages are usually somewhat out of date: 47 | 48 | * [Nightly builds for Debian, Ubuntu, SUSE and Fedora](https://build.opensuse.org/package/show/home:phillipberndt/pqiv) 49 | thanks to the OpenSUSE build service 50 | * [Alpine package](https://pkgs.alpinelinux.org/package/edge/testing/x86/pqiv) 51 | * [Arch package](https://www.archlinux.org/packages/community/x86_64/pqiv/) 52 | * [Debian package](https://packages.debian.org/en/sid/pqiv) 53 | * [FreeBSD port](https://www.freshports.org/graphics/pqiv/) 54 | * [Gentoo ebuild](https://packages.gentoo.org/packages/media-gfx/pqiv) 55 | * [macOS brew](https://formulae.brew.sh/formula/pqiv) 56 | * [NixOS package](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/pq/pqiv/package.nix) 57 | * [OpenBSD port](http://cvsweb.openbsd.org/cgi-bin/cvsweb/ports/graphics/pqiv/) 58 | * [OpenSUSE package](https://build.opensuse.org/package/show/openSUSE:Factory/pqiv) 59 | * [Void linux](https://github.com/void-linux/void-packages/tree/master/srcpkgs/pqiv) 60 | 61 | If you'd like to compile pqiv manually, you'll need 62 | 63 | * gtk+ 3.0 *or* gtk+ 2.6 64 | * gdk-pixbuf 2.2 (included in gtk+) 65 | * glib 2.32 (with gvfs for opening URLs) 66 | * cairo 1.6 67 | * Pango 1.10 68 | * gio 2.0 69 | * gdk 2.8 70 | 71 | and optionally also 72 | 73 | * ffmpeg / libav (for video support) 74 | * libarchive (for images in archives and cbX comic book files) 75 | * libspectre (any version, for ps/eps support) 76 | * libwebp (for WebP support) 77 | * MagickWand (any version, for additional image formats like psd) 78 | * poppler (any version, for pdf support) 79 | 80 | The backends are per default linked statically into the code, so all backend 81 | related build-time dependencies are also run-time dependencies. If you need a 82 | shared version of the backends, for example for separate packaging of the 83 | binaries or to make the run-time dependencies optional, use the 84 | `--backends-build=shared` configure option. 85 | 86 | For macOS, have a look at the `pqiv.app` target of the Makefile, too. 87 | 88 | pqiv can be linked statically, though GTK only supports static linking in 89 | GTK 2.x; in early versions of GTK 3.x it was fairly simple to still link 90 | statically. 91 | 92 | Windows builds are supported and work in GTK 2.x, it is recommended to use 93 | [MXE](https://mxe.cc/) for cross-compiling. 94 | 95 | Thanks 96 | ------ 97 | 98 | This program uses Martin Pool's natsort algorithm 99 | . 100 | 101 | Contributors 102 | ------------ 103 | 104 | Contributors to pqiv 2.x are: 105 | 106 | * J. Paul Reed 107 | * Chen Jonh L 108 | * Anton Älgmyr 109 | * Christian Garbs 110 | * Kanon Kubose 111 | * Wessel Dankers 112 | * @buzzingwires 113 | 114 | Contributors to pqiv ≤ 1.0 were: 115 | 116 | * Alexander Sulfrian 117 | * Alexandros Diamantidis 118 | * Brandon 119 | * David Lindquist 120 | * Hanspeter Gysin 121 | * John Keeping 122 | * Nir Tzachar 123 | * Rene Saarsoo 124 | * Tinoucas 125 | * Yaakov 126 | 127 | Known bugs 128 | ---------- 129 | 130 | * **The window is centered in between two monitors in old multi-head setups**: 131 | This happens if you have the RandR extension enabled, but configured 132 | incorrectly. GTK is programmed to first try RandR and use Xinerama only as 133 | a fallback if that fails. (See `gdkscreen-x11.c`.) So if your video drivers 134 | for some reason detect your multiple monitors as one big screen you can not 135 | simply use fakexinerama to fix things. This might also apply to nvidia drivers 136 | older than version 304. I believe that I can not fix this without breaking 137 | functionality for other users or maintaining a blacklist, so you should 138 | deactivate RandR completely until your driver is able to provide correct 139 | information, or use a fake xrand (like 140 | [mine](https://github.com/phillipberndt/fakexrandr), for example) 141 | 142 | * **Loading postscript files failes with `Error #12288; Unknown output format`**: 143 | This issue happens if your poppler and spectre libraries are linked against 144 | different versions of libcms. libcms and libcms2 will both be used, but 145 | interfere with each other. Compile using `--backends-build=shared` to 146 | circumvent this issue. 147 | 148 | Examples 149 | -------- 150 | 151 | Basic usage of pqiv is very straightforward, call 152 | 153 | pqiv 154 | 155 | and then use space, backspace, `f` (for fullscreen), `q` (to quit), and `m` for 156 | the montage overview to navigate through your images. To see all key bindings, 157 | see the `DEFAULT KEY BINDINGS` section of the man-page, or run 158 | `pqiv --show-bindings`. 159 | 160 | For some advanced uses of pqiv, take a look at these resouces: 161 | 162 | * [Play music while looking at specific images](https://github.com/phillipberndt/pqiv/issues/100#issuecomment-320651190) 163 | *
Bind keys to cycle through panels of a 2x2 comic 164 | 165 | Store this in your `.pqivrc`: 166 | ``` 167 | # Bind c to act as if "#c1" was typed 168 | c { send_keys(#c1); } 169 | # If "#c1" is typed, shift the current image to it's north west corner, and 170 | # rebind "c" to act as if "#c2" was typed 171 | c1 { set_shift_align_corner(NW); bind_key(c { send_keys(#c2\); }); } 172 | # ..etc.. 173 | c2 { set_shift_align_corner(NE); bind_key(c { send_keys(#c3\); }); } 174 | c3 { set_shift_align_corner(SW); bind_key(c { send_keys(#c4\); }); } 175 | # The last binding closes the cycle by rebinding "c" to act as if "#c1" was typed 176 | c4 { set_shift_align_corner(SE); bind_key(c { send_keys(#c1\); }); } 177 | ``` 178 | 179 |
180 | 181 | 182 | Changelog 183 | --------- 184 | 185 | pqiv (dev) 186 | * Fix YUVJ deprecation warning for ffmpeg (fixes #266) 187 | 188 | pqiv 2.13.3 189 | * Fix ffmpeg 8.0 compatibility (fixes #258) 190 | * Prefer specialized backends over generic ones (fixes #257) 191 | 192 | pqiv 2.13.2 193 | * Revert to not adding `--browse` to desktop files (fixes #232) 194 | * Fix crash for videos with unusual resolutions (fixes #247) 195 | 196 | pqiv 2.13 197 | * Fix `toggle_fullscreen(1/2)` behavior when already fullscreen 198 | * Add `--font` to adjust info box font, use Pango for rendering (See #221) 199 | * Prefer x11 over Wayland GDK backend (it overall provides a better experience) 200 | * Fix Client Side Decorations (CSD), e.g. in Wayland 201 | * Fix race/crash upon exit (Fixes #227) 202 | 203 |
204 | Click to expand changelog for old pqiv versions 205 | 206 | pqiv 2.12 207 | * Fix external image filters (Fixes #182) 208 | * Fix support for `best` interpolation quality (Fixes #139) 209 | * Fix wrap-around in shuffled image view (Fixes #176) 210 | * Fix max-depth behavior if the argument is a file (Fixes #170) 211 | * Allow keybinding of special keys with shift modifier 212 | * Add `--auto-montage-mode` (Fixes #181) 213 | * Replace GTimeVal with GDateTime for glib 2.62 support 214 | * Add an sxiv-like marks system 215 | 216 | pqiv 2.11 217 | * Added negate (color inversion) mode (bound to `n`, `--negate`) 218 | * Rebound `a` (hardlink image) to `c-a` by default (See #124) 219 | * Improved key bindings documentation (See #127) 220 | * Add `--actions-from-stdin` and let it block until actions are completed 221 | (See #118/#119) 222 | * Fix zooming on tiling WMs (See #129) 223 | * Support ffmpeg 4.0 API 224 | * Fix cross-compiling with X11 (Debian #913589) 225 | * Fix resizing in WMs without moveresize support (See #130) 226 | * Work around GTK bug resulting in crash due to invalid free() 227 | * Improve autotools compatibility of the configure script (See #135) 228 | 229 | pqiv 2.10.4 230 | * Fix output of `montage_mode_shift_y_rows()` in key bindings 231 | * Update the info text when the background pattern is cycled 232 | * Prevent potential crashes in poppler backend for rapid image movements 233 | * Fix processing of dangling symlinks in the file buffer 234 | * Removed possible deadlock in ImageMagick wand backend 235 | * Fix --command-9 shortcut 236 | * Makefile: Move -shared compiler flag to the end of the command line 237 | 238 | pqiv 2.10 239 | * Enable cursor auto-hide by default 240 | * Enable mouse navigation in montage mode 241 | * Added `toggle_background_pattern()` (bound to `b`) and 242 | `--background-pattern`. 243 | * Added support for alternate pqivrc paths, changed recommended location to 244 | ./.config/pqivrc. 245 | * Sped up `--low-memory` mode (using native- instead of image-surfaces) 246 | * Fixed graphical issues with fading mode and quick image transition 247 | * Fixed support for platforms with `sizeof(time_t) != sizeof(int)` 248 | * Fixed a race condition in the file buffer map 249 | 250 | pqiv 2.9 251 | * Added a montage/image grid mode (bound to `m` by default) 252 | * Added a [WebP](https://developers.google.com/speed/webp/) backend 253 | (by @john0312) 254 | * Added the means to skip over "logical" directories, such as archive files 255 | (bound to `ctrl+space` and `ctrl+backspace` by default) 256 | * Improved responsivity by caching pre-scaled copies of images 257 | * Removed tearing/flickering in WMs without extended frame sync support 258 | * Fixed support for huge images (>32,767px) in the GdkPixbuf backend 259 | * Added option --info-box-colors to customize the colors used in the info box 260 | * It is now possible to view --help even if no display is available 261 | * Added --version 262 | * Added an auto scale mode that maintains window size 263 | * Bound `Control+t` to switch to "maintain scale level" by default 264 | * Bound `Alt+t` to switch to "maintain window size" by default 265 | * Added action `move_window()` to explicitly move pqiv's main window around 266 | 267 | pqiv 2.8.5 268 | * Fixed an issue where the checkerboard pattern sometimes was visible at image 269 | borders 270 | * Fixed image rotation in low-memory mode 271 | * Fix a memory leak (leaking a few bytes each time an image is drawn) 272 | * Correctly handle string arguments from the configuration file 273 | * Fix building with old glib versions that do not expose their x11 dependency 274 | in pkgconfig 275 | * Fix support for duplicate files in sorted mode 276 | * Fix MagickWand exit handler code 277 | 278 | pqiv 2.8 279 | * Added option --allow-empty-window: Show pqiv even if no images can be loaded 280 | * Explicitly allow to load all files from a directory multiple times 281 | * Allow to use --libdir option in configure to override .so-files location 282 | * Fix shared-backend-pqiv in environments that compile with --enable-new-dtags 283 | * Enable the libav backend by default 284 | * Add option --disable-backends to disable backends at runtime 285 | 286 | pqiv 2.7.4 287 | * Fix GTK 2 compilation 288 | * Fix backends list in configure script 289 | * Fix race condition upon reloading animations 290 | * Fix Ctrl-R default binding (move `goto_earlier_file()` to Ctrl-P) 291 | 292 | pqiv 2.7 293 | * Fixed window decoration toggling with --transparent-background 294 | * Work around bug #67, poppler bug #96884 295 | * Added new action `set_interpolation_quality` to change interpolation/filter 296 | mode 297 | * pqiv now by default uses `nearest` interpolation for small images 298 | * Added actions and key bindings to control animation playback speed 299 | * Added a general archive backend for reading images from archives 300 | * Added a new action `goto_earlier_file()` to return to the image that was 301 | shown before the current one 302 | * Added a new action `set_cursor_auto_hide()` to automatically hide the pointer 303 | when it is not moved for some time 304 | * Support an `actions` section in the configuration file for default actions 305 | * Create and install a desktop file for pqiv during install 306 | * Disable GTK's transparent scaling on HiDpi monitors 307 | * New option --wait-for-images-to-appear to wait for images to appear if none 308 | are found 309 | 310 | pqiv 2.6 311 | * Added --enforce-window-aspect-ratio 312 | * Do not enforce the aspect ratio of the window to match the image's by default 313 | 314 | pqiv 2.5.1 315 | * Prevent a crash in --lazy-load mode if many images fail to load 316 | 317 | pqiv 2.5 318 | * Added a configure option to build the backends as shared libraries 319 | * Added a configure option to remove unneeded/unwanted features 320 | * Added --watch-files to make the file-changed-on-disk action configurable 321 | * Added support for cbz/cbr/cbt/cb7 comic books 322 | * Key bindings are now configurable 323 | * Deprecated --keyboard-alias and --reverse-cursor-keys in favor of 324 | --bind-key. 325 | * Added --actions-from-stdin to make pqiv scriptable 326 | * Added --recreate-window to create a new window instead of resizing the 327 | old one, as a workaround for buggy window managers 328 | * Fixed crash on reloading of images created by pipe-command output 329 | 330 | pqiv 2.4.1 331 | * Fix --end-of-files-action=quit if only one file is present 332 | * Fixed libav backend's pkg-config dependency list (by @onodera-punpun) 333 | * Enable image format support in the libav backend 334 | 335 | pqiv 2.4 336 | * Added --sort-key=mtime to sort by modification time instead of file name 337 | * Delay the "Image is still loading" message for half a second to avoid 338 | flickering status messages 339 | * Remove the "Image is still loading" message if --hide-info-box is set 340 | * Added [libav](https://www.ffmpeg.org/) backend for video support 341 | * Added --end-of-files-action=action to allow users to control what happens 342 | once all images have been viewed 343 | * Fix various minor memory allocation issues / possible race conditions 344 | 345 | pqiv 2.3.5 346 | * Fix parameters in pqivrc that are handled by a callback 347 | * Fix reference counting if an image fails to load 348 | * Properly reload multi-page files if they change on disk while being viewed 349 | * Properly handle if a user closes pqiv while the image loader is still active 350 | 351 | pqiv 2.3 352 | * Refactored an abstraction layer around the image backend 353 | * Added optional support for PDF-files through 354 | [poppler](http://poppler.freedesktop.org/) 355 | * Added optional support for PS-files through 356 | [libspectre](http://www.freedesktop.org/wiki/Software/libspectre/) 357 | * Added optional support for more image formats through 358 | [ImageMagick's MagickWand](http://www.imagemagick.org/script/magick-wand.php) 359 | * Support for gtk+ 3.14 360 | * configure/Makefile updated to support (Free-)BSD 361 | * Added ctrl + space/backspace hotkey for jumping to the next/previous directory 362 | * Improved pqiv's reaction if a file is removed 363 | * gtk 3.16 deprecates `gdk_cursor_new`, replaced by a different function 364 | * Shuffle mode is now toggleable at run-time (using Ctrl-R) 365 | 366 | pqiv 2.2 367 | * Accept URLs as command line arguments 368 | * Revived -r for reading additional files from stdin (by J.P. Reed) 369 | * Display the help message if invoked without parameters (by J.P. Reed) 370 | * Accept floating point slideshow intervals on the command line 371 | * Update the info box with the current numbers if (new) images are (un)loaded 372 | * Added --max-depth=n to limit how deep directories are searched 373 | * Added --browse to load, in addition to images from the command line, also 374 | all other images from the containing directories 375 | * Bugfix: Fixed handling of non-image command line arguments 376 | 377 | pqiv 2.1 378 | * Support for watching directories for new files 379 | * Downstream Makefile fix: Included LDFLAGS (from Gentoo package, by Tim 380 | Harder), updated for clean builds on OpenBSD (by jca[at]wxcvbn[dot]org, 381 | reported by github user @clod89) 382 | * Also included CPPFLAGS, for completeness 383 | * Renamed '.qiv-select' directory to '.pqiv-select' 384 | * Added a certain level of autoconf compatibility to the configure script, for 385 | automated building 386 | * gtk 3.10 stock icon deprecation issue fixed 387 | * Reimplemented fading between images 388 | * Display the last image while the current image has not been loaded 389 | * Gave users the option to abort the loading of huge images 390 | * Respect --shuffle and --sort with --watch-directories, i.e. insert keeping 391 | order, not always at the end 392 | * New option --lazy-load to display the main window while still traversing 393 | paths, searching for images 394 | * New option --low-memory to disable memory hungry features 395 | * Detect nested symlinks without preventing users from loading the same image 396 | multiple times 397 | * Improved cross-compilation support with mingw64 398 | 399 | pqiv 2.0 400 | * Complete rewrite from scratch 401 | * Based on GTK 3 and Cairo 402 | 403 | pqiv ≤ 1.0 404 | * See the old GTK 2 release for information on that 405 | (in the **gtk2** branch on github) 406 | 407 | pqiv ≤ 0.3 408 | * See the old python release for information on that 409 | (in the **python** branch on github) 410 | 411 |
412 | -------------------------------------------------------------------------------- /backends/libav.c: -------------------------------------------------------------------------------- 1 | /** 2 | * pqiv 3 | * 4 | * Copyright (c) 2013-2017, Phillip Berndt 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | * 18 | * libav backend 19 | */ 20 | 21 | /* This backend is based on the excellent short API example from 22 | http://hasanaga.info/tag/ffmpeg-libavcodec-avformat_open_input-example/ */ 23 | 24 | #include "../pqiv.h" 25 | #include "../lib/filebuffer.h" 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1) 34 | #define av_frame_alloc avcodec_alloc_frame 35 | #define av_frame_free avcodec_free_frame 36 | #endif 37 | 38 | #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(57, 0, 0) 39 | #define av_packet_unref av_free_packet 40 | #endif 41 | 42 | #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(54, 0, 0) 43 | #define avcodec_free_frame av_free 44 | #define AV_PIX_FMT_RGB32 PIX_FMT_RGB32 45 | #endif 46 | 47 | #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 41, 0) 48 | #define AV_COMPAT_CODEC_DEPRECATED 49 | #endif 50 | 51 | #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(58, 9, 100) 52 | #define AV_API_NEXT_CHANGES 53 | #endif 54 | 55 | // This is a list of extensions that are never handled by this backend 56 | // It is not a complete list of audio formats supported by ffmpeg, 57 | // only those I recognized right away. 58 | static const char * const ignore_extensions[] = { 59 | "aac", "ac3", "aiff", "bin", "dts", "flac", "gsm", "m4a", "mp3", "ogg", "f64be", "f64le", 60 | "f32be", "f32le", "s32be", "s32le", "s24be", "s24le", "s16be", "s16le", "s8", 61 | "u32be", "u32le", "u24be", "u24le", "u16be", "u16le", "u8", "sox", "spdif", "txt", 62 | "w64", "wav", "xa", "xwma", NULL 63 | }; 64 | 65 | typedef struct { 66 | GBytes *file_data; 67 | gsize file_data_pos; 68 | 69 | AVFormatContext *avcontext; 70 | AVIOContext *aviocontext; 71 | AVCodecContext *cocontext; 72 | int video_stream_id; 73 | 74 | gboolean pkt_valid; 75 | AVPacket pkt; 76 | 77 | AVFrame *frame; 78 | AVFrame *rgb_frame; 79 | uint8_t *buffer; 80 | 81 | guint pixel_width; 82 | guint pixel_height; 83 | AVRational sample_aspect_ratio; 84 | } file_private_data_libav_t; 85 | 86 | static int file_type_libav_memory_access_reader(void *opaque, uint8_t *buf, int buf_size) {/*{{{*/ 87 | file_private_data_libav_t *private = opaque; 88 | 89 | gsize data_size = 0; 90 | gconstpointer data = g_bytes_get_data(private->file_data, &data_size); 91 | 92 | if(buf_size < 0) { 93 | return -1; 94 | } 95 | 96 | if((unsigned)buf_size > data_size - private->file_data_pos) { 97 | buf_size = data_size - private->file_data_pos; 98 | } 99 | 100 | if(private->file_data_pos < data_size) { 101 | memcpy(buf, (const char*)data + private->file_data_pos, buf_size); 102 | private->file_data_pos += buf_size; 103 | } 104 | 105 | return buf_size; 106 | }/*}}}*/ 107 | 108 | static int64_t file_type_libav_memory_access_seeker(void *opaque, int64_t offset, int whence) {/*{{{*/ 109 | file_private_data_libav_t *private = opaque; 110 | whence &= (SEEK_CUR | SEEK_SET | SEEK_END); 111 | 112 | gsize data_size = 0; 113 | g_bytes_get_data(private->file_data, &data_size); 114 | 115 | switch(whence) { 116 | case SEEK_CUR: 117 | if(0 <= (ssize_t)private->file_data_pos + offset && private->file_data_pos + offset < data_size) { 118 | private->file_data_pos += offset; 119 | } 120 | return 0; 121 | break; 122 | 123 | case SEEK_SET: 124 | if(offset >= 0 && offset < (ssize_t)data_size) { 125 | private->file_data_pos = offset; 126 | } 127 | return 0; 128 | break; 129 | 130 | case SEEK_END: 131 | if(offset <= 0) { 132 | private->file_data_pos = data_size + offset; 133 | } 134 | break; 135 | } 136 | 137 | return -1; 138 | }/*}}}*/ 139 | 140 | BOSNode *file_type_libav_alloc(load_images_state_t state, file_t *file) {/*{{{*/ 141 | file->private = g_slice_new0(file_private_data_libav_t); 142 | return load_images_handle_parameter_add_file(state, file); 143 | }/*}}}*/ 144 | void file_type_libav_free(file_t *file) {/*{{{*/ 145 | g_slice_free(file_private_data_libav_t, file->private); 146 | }/*}}}*/ 147 | void file_type_libav_unload(file_t *file) {/*{{{*/ 148 | file_private_data_libav_t *private = (file_private_data_libav_t *)file->private; 149 | 150 | if(private->file_data) { 151 | g_bytes_unref(private->file_data); 152 | buffered_file_unref(file); 153 | private->file_data = NULL; 154 | private->file_data_pos = 0; 155 | } 156 | 157 | if(private->pkt_valid) { 158 | av_packet_unref(&(private->pkt)); 159 | private->pkt_valid = FALSE; 160 | } 161 | 162 | if(private->frame) { 163 | av_frame_free(&(private->frame)); 164 | } 165 | 166 | if(private->rgb_frame) { 167 | av_frame_free(&(private->rgb_frame)); 168 | } 169 | 170 | if(private->avcontext) { 171 | #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(55,53,0) 172 | avcodec_free_context(&(private->cocontext)); 173 | #else 174 | av_freep(&(private->cocontext)); 175 | #endif 176 | avformat_close_input(&(private->avcontext)); 177 | } 178 | 179 | if(private->aviocontext) { 180 | av_freep(&private->aviocontext->buffer); 181 | av_freep(&private->aviocontext); 182 | private->aviocontext = NULL; 183 | } 184 | 185 | if(private->buffer) { 186 | g_free(private->buffer); 187 | private->buffer = NULL; 188 | } 189 | }/*}}}*/ 190 | void file_type_libav_load(file_t *file, GInputStream *data, GError **error_pointer) {/*{{{*/ 191 | file_private_data_libav_t *private = (file_private_data_libav_t *)file->private; 192 | 193 | if(private->avcontext) { 194 | // Double check if the file was properly freed. It is an error if it was not, the check is merely 195 | // here because libav crashes if it was not. 196 | assert(!private->avcontext); 197 | file_type_libav_unload(file); 198 | } 199 | 200 | if(file->file_flags & FILE_FLAGS_MEMORY_IMAGE) { 201 | if(!private->file_data) { 202 | private->file_data = buffered_file_as_bytes(file, data, error_pointer); 203 | } 204 | private->file_data_pos = 0; 205 | 206 | private->avcontext = avformat_alloc_context(); 207 | private->aviocontext = avio_alloc_context(av_malloc(4096), 4096, 0, private, &file_type_libav_memory_access_reader, NULL, &file_type_libav_memory_access_seeker); 208 | private->avcontext->pb = private->aviocontext; 209 | if(avformat_open_input(&(private->avcontext), NULL, NULL, NULL) < 0) { 210 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-libav-error"), 1, "Failed to load image using libav."); 211 | return; 212 | } 213 | } 214 | else { 215 | if(avformat_open_input(&(private->avcontext), file->file_name, NULL, NULL) < 0) { 216 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-libav-error"), 1, "Failed to load image using libav."); 217 | return; 218 | } 219 | } 220 | 221 | if(avformat_find_stream_info(private->avcontext, NULL) < 0) { 222 | avformat_close_input(&(private->avcontext)); 223 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-libav-error"), 1, "Failed to load image using libav."); 224 | return; 225 | } 226 | 227 | private->video_stream_id = -1; 228 | for(size_t i=0; iavcontext->nb_streams; i++) { 229 | if( 230 | #ifndef AV_COMPAT_CODEC_DEPRECATED 231 | private->avcontext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO 232 | #else 233 | private->avcontext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO 234 | #endif 235 | ) { 236 | private->video_stream_id = i; 237 | break; 238 | } 239 | } 240 | if(private->video_stream_id < 0 || ( 241 | #ifndef AV_COMPAT_CODEC_DEPRECATED 242 | private->avcontext->streams[private->video_stream_id]->codec->width == 0 243 | #else 244 | private->avcontext->streams[private->video_stream_id]->codecpar->width == 0 245 | #endif 246 | )) { 247 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-libav-error"), 1, "This is not a video file."); 248 | avformat_close_input(&(private->avcontext)); 249 | return; 250 | } 251 | #ifndef AV_COMPAT_CODEC_DEPRECATED 252 | AVCodec *codec = avcodec_find_decoder(private->avcontext->streams[private->video_stream_id]->codec->codec_id); 253 | #else 254 | const AVCodec *codec = avcodec_find_decoder(private->avcontext->streams[private->video_stream_id]->codecpar->codec_id); 255 | #endif 256 | private->cocontext = avcodec_alloc_context3(codec); 257 | #ifndef AV_COMPAT_CODEC_DEPRECATED 258 | avcodec_copy_context(private->cocontext, private->avcontext->streams[private->video_stream_id]->codec); 259 | #else 260 | avcodec_parameters_to_context(private->cocontext, private->avcontext->streams[private->video_stream_id]->codecpar); 261 | #endif 262 | if(!codec || avcodec_open2(private->cocontext, codec, NULL) < 0) { 263 | *error_pointer = g_error_new(g_quark_from_static_string("pqiv-libav-error"), 1, "Failed to open codec."); 264 | avformat_close_input(&(private->avcontext)); 265 | return; 266 | } 267 | 268 | private->frame = av_frame_alloc(); 269 | private->rgb_frame = av_frame_alloc(); 270 | 271 | file->file_flags |= FILE_FLAGS_ANIMATION; 272 | #ifdef AV_COMPAT_CODEC_DEPRECATED 273 | private->pixel_width = private->avcontext->streams[private->video_stream_id]->codecpar->width; 274 | private->pixel_height = private->avcontext->streams[private->video_stream_id]->codecpar->height; 275 | private->sample_aspect_ratio = private->avcontext->streams[private->video_stream_id]->codecpar->sample_aspect_ratio; 276 | #else 277 | private->pixel_width = private->avcontext->streams[private->video_stream_id]->codec->width; 278 | private->pixel_height = private->avcontext->streams[private->video_stream_id]->codec->height; 279 | private->sample_aspect_ratio = private->avcontext->streams[private->video_stream_id]->codec->sample_aspect_ratio; 280 | #endif 281 | if(private->sample_aspect_ratio.num == 0 || private->sample_aspect_ratio.den == 0) { 282 | private->sample_aspect_ratio.num = private->sample_aspect_ratio.den = 1; 283 | file->width = private->pixel_width; 284 | file->height = private->pixel_height; 285 | } 286 | else if(private->sample_aspect_ratio.num > private->sample_aspect_ratio.den) { 287 | file->width = private->sample_aspect_ratio.num * private->pixel_width / private->sample_aspect_ratio.den; 288 | file->height = private->pixel_height; 289 | } 290 | else { 291 | file->width = private->pixel_width; 292 | file->height = private->sample_aspect_ratio.den * private->pixel_height / private->sample_aspect_ratio.num; 293 | } 294 | 295 | #if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(55, 0, 0) 296 | size_t num_bytes = avpicture_get_size(PIX_FMT_RGB32, file->width, file->height); 297 | #else 298 | size_t num_bytes = av_image_get_buffer_size(AV_PIX_FMT_RGB32, file->width, file->height, 64); 299 | #endif 300 | private->buffer = (uint8_t *)g_malloc(num_bytes * sizeof(uint8_t)); 301 | 302 | if(file->width == 0 || file->height == 0) { 303 | file_type_libav_unload(file); 304 | file->is_loaded = FALSE; 305 | return; 306 | } 307 | file->is_loaded = TRUE; 308 | }/*}}}*/ 309 | double file_type_libav_animation_next_frame(file_t *file) {/*{{{*/ 310 | file_private_data_libav_t *private = (file_private_data_libav_t *)file->private; 311 | 312 | if(!private->avcontext) { 313 | return -1; 314 | } 315 | 316 | AVPacket old_pkt = private->pkt; 317 | 318 | do { 319 | // Loop until the next video frame is found 320 | memset(&(private->pkt), 0, sizeof(AVPacket)); 321 | if(av_read_frame(private->avcontext, &(private->pkt)) < 0) { 322 | av_packet_unref(&(private->pkt)); 323 | if(avformat_seek_file(private->avcontext, -1, 0, 0, 1, 0) < 0 || av_read_frame(private->avcontext, &(private->pkt)) < 0) { 324 | // Reading failed; end stream here to be on the safe side 325 | // Display last frame to the user 326 | private->pkt = old_pkt; 327 | return -1; 328 | } 329 | } 330 | } while(private->pkt.stream_index != private->video_stream_id); 331 | 332 | if(private->pkt_valid) { 333 | av_packet_unref(&old_pkt); 334 | } 335 | else { 336 | private->pkt_valid = TRUE; 337 | } 338 | 339 | AVFrame *frame = private->frame; 340 | #ifndef AV_COMPAT_CODEC_DEPRECATED 341 | int got_picture_ptr = 0; 342 | avcodec_decode_video2(private->cocontext, frame, &got_picture_ptr, &(private->pkt)); 343 | #else 344 | if(avcodec_send_packet(private->cocontext, &(private->pkt)) >= 0) { 345 | avcodec_receive_frame(private->cocontext, frame); 346 | } 347 | #endif 348 | 349 | if(private->avcontext->streams[private->video_stream_id]->avg_frame_rate.den != 0 && private->avcontext->streams[private->video_stream_id]->avg_frame_rate.num != 0) { 350 | // Stream has reliable average framerate 351 | return 1000. * private->avcontext->streams[private->video_stream_id]->avg_frame_rate.den / private->avcontext->streams[private->video_stream_id]->avg_frame_rate.num; 352 | } 353 | else if(private->avcontext->streams[private->video_stream_id]->time_base.den != 0 && private->avcontext->streams[private->video_stream_id]->time_base.num != 0) { 354 | // Stream has usable time base 355 | return private->pkt.duration * private->avcontext->streams[private->video_stream_id]->time_base.num * 1000. / private->avcontext->streams[private->video_stream_id]->time_base.den; 356 | } 357 | 358 | // TODO What could be done here as a last fallback?! -> Figure this out from ffmpeg! 359 | return 10; 360 | }/*}}}*/ 361 | double file_type_libav_animation_initialize(file_t *file) {/*{{{*/ 362 | return file_type_libav_animation_next_frame(file); 363 | }/*}}}*/ 364 | void file_type_libav_draw(file_t *file, cairo_t *cr) {/*{{{*/ 365 | file_private_data_libav_t *private = (file_private_data_libav_t *)file->private; 366 | 367 | if(private->pkt_valid) { 368 | AVFrame *frame = private->frame; 369 | AVFrame *rgb_frame = private->rgb_frame; 370 | 371 | #ifdef AV_COMPAT_CODEC_DEPRECATED 372 | int pix_fmt = private->avcontext->streams[private->video_stream_id]->codecpar->format; 373 | #else 374 | int pix_fmt = private->avcontext->streams[private->video_stream_id]->codec->pix_fmt; 375 | #endif 376 | 377 | int color_space_needs_patching = FALSE; 378 | switch (pix_fmt) { 379 | case AV_PIX_FMT_YUVJ420P: 380 | pix_fmt = AV_PIX_FMT_YUV420P; 381 | color_space_needs_patching = TRUE; 382 | break; 383 | case AV_PIX_FMT_YUVJ422P: 384 | pix_fmt = AV_PIX_FMT_YUV422P; 385 | color_space_needs_patching = TRUE; 386 | break; 387 | case AV_PIX_FMT_YUVJ440P: 388 | pix_fmt = AV_PIX_FMT_YUV440P; 389 | color_space_needs_patching = TRUE; 390 | break; 391 | case AV_PIX_FMT_YUVJ444P: 392 | pix_fmt = AV_PIX_FMT_YUV444P; 393 | color_space_needs_patching = TRUE; 394 | break; 395 | default: 396 | break; 397 | } 398 | 399 | // Prepare buffer for RGB32 version 400 | uint8_t *buffer = private->buffer; 401 | #if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(55, 0, 0) 402 | avpicture_fill((AVPicture *)rgb_frame, buffer, PIX_FMT_RGB32, file->width, file->height); 403 | #else 404 | av_image_fill_arrays(rgb_frame->data, rgb_frame->linesize, buffer, AV_PIX_FMT_RGB32, file->width, file->height, 16); 405 | #endif 406 | 407 | // If a valid frame is available.. 408 | if(frame->data[0]) { 409 | // ..convert to RGB32 410 | struct SwsContext *img_convert_ctx = sws_getCachedContext(NULL, private->pixel_width, private->pixel_height, pix_fmt, file->width, 411 | file->height, AV_PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL); 412 | 413 | if(color_space_needs_patching) { 414 | // Following https://stackoverflow.com/questions/23067722 415 | // Needed to use YUV for YUVJ 416 | int dummy[4]; 417 | int src_range, dst_range; 418 | int brightness, contrast, saturation; 419 | sws_getColorspaceDetails(img_convert_ctx, (int**)&dummy, &src_range, (int**)&dummy, &dst_range, &brightness, &contrast, &saturation); 420 | const int* coefs = sws_getCoefficients(SWS_CS_DEFAULT); 421 | src_range = 1; 422 | sws_setColorspaceDetails(img_convert_ctx, coefs, src_range, coefs, dst_range, 423 | brightness, contrast, saturation); 424 | } 425 | 426 | 427 | sws_scale(img_convert_ctx, (const uint8_t * const*)frame->data, frame->linesize, 0, private->pixel_height, rgb_frame->data, rgb_frame->linesize); 428 | sws_freeContext(img_convert_ctx); 429 | } 430 | 431 | // Draw to a temporary image surface and then to cr 432 | cairo_surface_t *image_surface = cairo_image_surface_create_for_data(rgb_frame->data[0], CAIRO_FORMAT_ARGB32, file->width, file->height, rgb_frame->linesize[0]); 433 | cairo_set_source_surface(cr, image_surface, 0, 0); 434 | apply_interpolation_quality(cr); 435 | cairo_paint(cr); 436 | cairo_surface_destroy(image_surface); 437 | } 438 | }/*}}}*/ 439 | static gboolean _is_ignored_extension(const char *extension) {/*{{{*/ 440 | for(const char * const * ext = ignore_extensions; *ext; ext++) { 441 | if(strcmp(*ext, extension) == 0) { 442 | return TRUE; 443 | } 444 | } 445 | return FALSE; 446 | }/*}}}*/ 447 | void file_type_libav_initializer(file_type_handler_t *info) {/*{{{*/ 448 | #ifndef AV_API_NEXT_CHANGES 449 | avcodec_register_all(); 450 | av_register_all(); 451 | #else 452 | void *opaque_iter = NULL; 453 | #endif 454 | avformat_network_init(); 455 | 456 | // Register all file formats supported by libavformat 457 | info->file_types_handled = gtk_file_filter_new(); 458 | #ifndef AV_API_NEXT_CHANGES 459 | for(AVInputFormat *iter = av_iformat_next(NULL); iter; iter = av_iformat_next(iter)) 460 | #else 461 | for(const AVInputFormat *iter; (iter = av_demuxer_iterate(&opaque_iter)); /*nothing */) 462 | #endif 463 | { 464 | if(iter->name) { 465 | gchar **fmts = g_strsplit(iter->name, ",", -1); 466 | for(gchar **fmt = fmts; *fmt; fmt++) { 467 | if(_is_ignored_extension(*fmt)) { 468 | continue; 469 | } 470 | gchar *format = g_strdup_printf("*.%s", *fmt); 471 | gtk_file_filter_add_pattern(info->file_types_handled, format); 472 | g_free(format); 473 | } 474 | g_strfreev(fmts); 475 | } 476 | 477 | if(iter->extensions) { 478 | gchar **fmts = g_strsplit(iter->extensions, ",", -1); 479 | for(gchar **fmt = fmts; *fmt; fmt++) { 480 | if(_is_ignored_extension(*fmt)) { 481 | continue; 482 | } 483 | gchar *format = g_strdup_printf("*.%s", *fmt); 484 | gtk_file_filter_add_pattern(info->file_types_handled, format); 485 | g_free(format); 486 | } 487 | g_strfreev(fmts); 488 | } 489 | } 490 | 491 | // Register as general handler for video/* MIME types 492 | gtk_file_filter_add_mime_type(info->file_types_handled, "video/*"); 493 | 494 | // Assign the handlers 495 | info->alloc_fn = file_type_libav_alloc; 496 | info->free_fn = file_type_libav_free; 497 | info->load_fn = file_type_libav_load; 498 | info->unload_fn = file_type_libav_unload; 499 | info->animation_initialize_fn = file_type_libav_animation_initialize; 500 | info->animation_next_frame_fn = file_type_libav_animation_next_frame; 501 | info->draw_fn = file_type_libav_draw; 502 | }/*}}}*/ 503 | -------------------------------------------------------------------------------- /lib/bostree.c: -------------------------------------------------------------------------------- 1 | /* 2 | Self-Balancing order statistic tree 3 | 4 | Implements an AVL tree with two additional methods, 5 | Select(i), which finds the i'th smallest element, and 6 | Rank(x), which returns the rank of a given element, 7 | which both run in O(log n). 8 | 9 | The tree is implemented with map semantics, that is, there are separete key 10 | and value pointers. 11 | 12 | Copyright (c) 2017, Phillip Berndt 13 | 14 | This program is free software: you can redistribute it and/or modify 15 | it under the terms of the GNU General Public License as published by 16 | the Free Software Foundation, either version 3 of the License, or 17 | (at your option) any later version. 18 | 19 | This program is distributed in the hope that it will be useful, 20 | but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | GNU General Public License for more details. 23 | 24 | You should have received a copy of the GNU General Public License 25 | along with this program. If not, see . 26 | */ 27 | 28 | #include "bostree.h" 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | /* Tree structure */ 35 | struct _BOSTree { 36 | BOSNode *root_node; 37 | 38 | BOSTree_cmp_function cmp_function; 39 | BOSTree_free_function free_function; 40 | }; 41 | 42 | /* Local helper functions */ 43 | static int _imax(const int i1, const int i2) { 44 | return i1 > i2 ? i1 : i2; 45 | } 46 | 47 | static int _bostree_balance(BOSNode *node) { 48 | const int left_depth = node->left_child_node ? node->left_child_node->depth + 1 : 0; 49 | const int right_depth = node->right_child_node ? node->right_child_node->depth + 1 : 0; 50 | return right_depth - left_depth; 51 | } 52 | 53 | static BOSNode *_bostree_rotate_right(BOSTree *tree, BOSNode *P) { 54 | // Rotate right: 55 | // 56 | // P L 57 | // L R --> c1 P 58 | //c1 c2 c2 R 59 | // 60 | BOSNode *L = P->left_child_node; 61 | 62 | if(P->parent_node) { 63 | if(P->parent_node->left_child_node == P) { 64 | P->parent_node->left_child_node = L; 65 | } 66 | else { 67 | P->parent_node->right_child_node = L; 68 | } 69 | } 70 | else { 71 | tree->root_node = L; 72 | } 73 | 74 | L->parent_node = P->parent_node; 75 | 76 | P->left_child_node = L->right_child_node; 77 | P->left_child_count = L->right_child_count; 78 | if(P->left_child_node) { 79 | P->left_child_node->parent_node = P; 80 | } 81 | P->depth = _imax(P->left_child_node ? 1 + P->left_child_node->depth : 0, P->right_child_node ? 1 + P->right_child_node->depth : 0); 82 | P->parent_node = L; 83 | 84 | L->right_child_node = P; 85 | P->parent_node = L; 86 | L->right_child_count = P->left_child_count + P->right_child_count + 1; 87 | L->depth = _imax(L->left_child_node ? 1 + L->left_child_node->depth : 0, L->right_child_node ? 1 + L->right_child_node->depth : 0); 88 | 89 | return L; 90 | } 91 | 92 | static BOSNode *_bostree_rotate_left(BOSTree *tree, BOSNode *P) { 93 | // Rotate left: 94 | // 95 | // P R 96 | // L R --> P c2 97 | // c1 c2 L c1 98 | // 99 | BOSNode *R = P->right_child_node; 100 | 101 | if(P->parent_node) { 102 | if(P->parent_node->left_child_node == P) { 103 | P->parent_node->left_child_node = R; 104 | } 105 | else { 106 | P->parent_node->right_child_node = R; 107 | } 108 | } 109 | else { 110 | tree->root_node = R; 111 | } 112 | 113 | R->parent_node = P->parent_node; 114 | 115 | P->right_child_node = R->left_child_node; 116 | P->right_child_count = R->left_child_count; 117 | if(P->right_child_node) { 118 | P->right_child_node->parent_node = P; 119 | } 120 | P->depth = _imax(P->left_child_node ? 1 + P->left_child_node->depth : 0, P->right_child_node ? 1 + P->right_child_node->depth : 0); 121 | P->parent_node = R; 122 | 123 | R->left_child_node = P; 124 | P->parent_node = R; 125 | R->left_child_count = P->left_child_count + P->right_child_count + 1; 126 | R->depth = _imax(R->left_child_node ? 1 + R->left_child_node->depth : 0, R->right_child_node ? 1 + R->right_child_node->depth : 0); 127 | 128 | return R; 129 | } 130 | 131 | 132 | /* API implementation */ 133 | BOSTree *bostree_new(BOSTree_cmp_function cmp_function, BOSTree_free_function free_function) { 134 | BOSTree *new_tree = malloc(sizeof(BOSTree)); 135 | new_tree->root_node = NULL; 136 | new_tree->cmp_function = cmp_function; 137 | new_tree->free_function = free_function; 138 | return new_tree; 139 | } 140 | 141 | void bostree_destroy(BOSTree *tree) { 142 | // Walk the tree and unref all nodes 143 | BOSNode *node = tree->root_node; 144 | while(node) { 145 | // The order should not really matter, but use post-order traversal here. 146 | while(node->left_child_node) { 147 | node = node->left_child_node; 148 | } 149 | 150 | if(node->right_child_node) { 151 | node = node->right_child_node; 152 | continue; 153 | } 154 | 155 | // At this point, we can be sure that this node has no child nodes. 156 | // So it is safe to remove it. 157 | BOSNode *next = node->parent_node; 158 | if(next) { 159 | if(next->left_child_node == node) { 160 | next->left_child_node = NULL; 161 | } 162 | else { 163 | next->right_child_node = NULL; 164 | } 165 | } 166 | bostree_node_weak_unref(tree, node); 167 | node = next; 168 | } 169 | 170 | free(tree); 171 | } 172 | 173 | unsigned int bostree_node_count(BOSTree *tree) { 174 | return tree->root_node ? tree->root_node->left_child_count + tree->root_node->right_child_count + 1 : 0; 175 | } 176 | 177 | BOSNode *bostree_insert(BOSTree *tree, void *key, void *data) { 178 | BOSNode **node = &tree->root_node; 179 | BOSNode *parent_node = NULL; 180 | 181 | // Find tree position to insert new node 182 | while(*node) { 183 | parent_node = *node; 184 | int cmp = tree->cmp_function(key, (*node)->key); 185 | if(cmp < 0) { 186 | (*node)->left_child_count++; 187 | node = &(*node)->left_child_node; 188 | } 189 | else { 190 | (*node)->right_child_count++; 191 | node = &(*node)->right_child_node; 192 | } 193 | } 194 | 195 | // Create new node 196 | BOSNode *new_node = malloc(sizeof(BOSNode)); 197 | memset(new_node, 0, sizeof(BOSNode)); 198 | new_node->key = key; 199 | new_node->data = data; 200 | new_node->weak_ref_count = 1; 201 | new_node->weak_ref_node_valid = 1; 202 | new_node->parent_node = parent_node; 203 | 204 | *node = new_node; 205 | 206 | if(!parent_node) { 207 | // Simple case, this is the first node. 208 | tree->root_node = new_node; 209 | return new_node; 210 | } 211 | 212 | // Check if the depth changed with the new node: 213 | // It does only change if this is the first child of the parent 214 | if(!!parent_node->left_child_node ^ !!parent_node->right_child_node) { 215 | // Bubble the information up the tree 216 | parent_node->depth++; 217 | while(parent_node->parent_node) { 218 | // Assign new depth 219 | parent_node = parent_node->parent_node; 220 | 221 | unsigned int new_left_depth = parent_node->left_child_node ? parent_node->left_child_node->depth + 1 : 0; 222 | unsigned int new_right_depth = parent_node->right_child_node ? parent_node->right_child_node->depth + 1 : 0; 223 | 224 | unsigned int max_depth = _imax(new_left_depth, new_right_depth); 225 | 226 | if(parent_node->depth != max_depth) { 227 | parent_node->depth = max_depth; 228 | } 229 | else { 230 | // We can break here, because if the depth did not change 231 | // at this level, it won't have further up either. 232 | break; 233 | } 234 | 235 | // Check if this node violates the AVL property, that is, that the 236 | // depths differ by no more than 1. 237 | if(new_left_depth - 2 == new_right_depth) { 238 | // Handle left-right case 239 | if(_bostree_balance(parent_node->left_child_node) > 0) { 240 | _bostree_rotate_left(tree, parent_node->left_child_node); 241 | } 242 | 243 | // Left is two levels deeper than right. Rotate right. 244 | parent_node = _bostree_rotate_right(tree, parent_node); 245 | } 246 | else if(new_left_depth + 2 == new_right_depth) { 247 | // Handle right-left case 248 | if(_bostree_balance(parent_node->right_child_node) < 0) { 249 | _bostree_rotate_right(tree, parent_node->right_child_node); 250 | } 251 | 252 | // Right is two levels deeper than left. Rotate left. 253 | parent_node = _bostree_rotate_left(tree, parent_node); 254 | } 255 | } 256 | } 257 | 258 | return new_node; 259 | } 260 | 261 | void bostree_remove(BOSTree *tree, BOSNode *node) { 262 | BOSNode *bubble_up = NULL; 263 | 264 | // If this node has children on both sides, bubble one of it upwards 265 | // and rotate within the subtrees. 266 | if(node->left_child_node && node->right_child_node) { 267 | BOSNode *candidate = NULL; 268 | BOSNode *lost_child = NULL; 269 | if(node->left_child_node->depth >= node->right_child_node->depth) { 270 | // Left branch is deeper than right branch, might be a good idea to 271 | // bubble from this side to maintain the AVL property with increased 272 | // likelihood. 273 | node->left_child_count--; 274 | candidate = node->left_child_node; 275 | while(candidate->right_child_node) { 276 | candidate->right_child_count--; 277 | candidate = candidate->right_child_node; 278 | } 279 | lost_child = candidate->left_child_node; 280 | } 281 | else { 282 | node->right_child_count--; 283 | candidate = node->right_child_node; 284 | while(candidate->left_child_node) { 285 | candidate->left_child_count--; 286 | candidate = candidate->left_child_node; 287 | } 288 | lost_child = candidate->right_child_node; 289 | } 290 | 291 | BOSNode *bubble_start = candidate->parent_node; 292 | if(bubble_start->left_child_node == candidate) { 293 | bubble_start->left_child_node = lost_child; 294 | } 295 | else { 296 | bubble_start->right_child_node = lost_child; 297 | } 298 | if(lost_child) { 299 | lost_child->parent_node = bubble_start; 300 | } 301 | 302 | // We will later rebalance upwards from bubble_start up to candidate. 303 | // But first, anchor candidate into the place where "node" used to be. 304 | 305 | if(node->parent_node) { 306 | if(node->parent_node->left_child_node == node) { 307 | node->parent_node->left_child_node = candidate; 308 | } 309 | else { 310 | node->parent_node->right_child_node = candidate; 311 | } 312 | } 313 | else { 314 | tree->root_node = candidate; 315 | } 316 | candidate->parent_node = node->parent_node; 317 | 318 | candidate->left_child_node = node->left_child_node; 319 | candidate->left_child_count = node->left_child_count; 320 | candidate->right_child_node = node->right_child_node; 321 | candidate->right_child_count = node->right_child_count; 322 | 323 | if(candidate->left_child_node) { 324 | candidate->left_child_node->parent_node = candidate; 325 | } 326 | 327 | if(candidate->right_child_node) { 328 | candidate->right_child_node->parent_node = candidate; 329 | } 330 | 331 | // From here on, node is out of the game. 332 | // Rebalance up to candidate. 333 | 334 | if(bubble_start != node) { 335 | while(bubble_start != candidate) { 336 | bubble_start->depth = _imax((bubble_start->left_child_node ? bubble_start->left_child_node->depth + 1 : 0), 337 | (bubble_start->right_child_node ? bubble_start->right_child_node->depth + 1 : 0)); 338 | int balance = _bostree_balance(bubble_start); 339 | if(balance > 1) { 340 | // Rotate left. Check for right-left case before. 341 | if(_bostree_balance(bubble_start->right_child_node) < 0) { 342 | _bostree_rotate_right(tree, bubble_start->right_child_node); 343 | } 344 | bubble_start = _bostree_rotate_left(tree, bubble_start); 345 | } 346 | else if(balance < -1) { 347 | // Rotate right. Check for left-right case before. 348 | if(_bostree_balance(bubble_start->left_child_node) > 0) { 349 | _bostree_rotate_left(tree, bubble_start->left_child_node); 350 | } 351 | bubble_start = _bostree_rotate_right(tree, bubble_start); 352 | } 353 | bubble_start = bubble_start->parent_node; 354 | } 355 | } 356 | 357 | // Fixup candidate's depth 358 | candidate->depth = _imax((candidate->left_child_node ? candidate->left_child_node->depth + 1 : 0), 359 | (candidate->right_child_node ? candidate->right_child_node->depth + 1 : 0)); 360 | 361 | // We'll have to fixup child counts and depths up to the root, do that 362 | // later. 363 | bubble_up = candidate->parent_node; 364 | 365 | // Fix immediate parent node child count here. 366 | if(bubble_up) { 367 | if(bubble_up->left_child_node == candidate) { 368 | bubble_up->left_child_count--; 369 | } 370 | else { 371 | bubble_up->right_child_count--; 372 | } 373 | } 374 | } 375 | else { 376 | // If this node has children on one side only, removing it is much simpler. 377 | if(!node->parent_node) { 378 | // Simple case: Node _was_ the old root. 379 | if(node->left_child_node) { 380 | tree->root_node = node->left_child_node; 381 | if(node->left_child_node) { 382 | node->left_child_node->parent_node = NULL; 383 | } 384 | } 385 | else { 386 | tree->root_node = node->right_child_node; 387 | if(node->right_child_node) { 388 | node->right_child_node->parent_node = NULL; 389 | } 390 | } 391 | 392 | // No rebalancing to do 393 | bubble_up = NULL; 394 | } 395 | else { 396 | BOSNode *candidate = node->left_child_node; 397 | int candidate_count = node->left_child_count; 398 | if(node->right_child_node) { 399 | candidate = node->right_child_node; 400 | candidate_count = node->right_child_count; 401 | } 402 | 403 | if(node->parent_node->left_child_node == node) { 404 | node->parent_node->left_child_node = candidate; 405 | node->parent_node->left_child_count = candidate_count; 406 | } 407 | else { 408 | node->parent_node->right_child_node = candidate; 409 | node->parent_node->right_child_count = candidate_count; 410 | } 411 | 412 | if(candidate) { 413 | candidate->parent_node = node->parent_node; 414 | } 415 | 416 | // Again, from here on, the original node is out of the game. 417 | // Rebalance up to the root. 418 | bubble_up = node->parent_node; 419 | } 420 | } 421 | 422 | // At this point, everything below and including bubble_start is 423 | // balanced, and we have to look further up. 424 | 425 | char bubbling_finished = 0; 426 | while(bubble_up) { 427 | if(!bubbling_finished) { 428 | // Calculate updated depth for bubble_up 429 | unsigned int left_depth = bubble_up->left_child_node ? bubble_up->left_child_node->depth + 1 : 0; 430 | unsigned int right_depth = bubble_up->right_child_node ? bubble_up->right_child_node->depth + 1 : 0; 431 | unsigned int new_depth = _imax(left_depth, right_depth); 432 | char depth_changed = (new_depth != bubble_up->depth); 433 | bubble_up->depth = new_depth; 434 | 435 | // Rebalance bubble_up 436 | // Not necessary for the first node, but calling _bostree_balance once 437 | // isn't that much overhead. 438 | int balance = _bostree_balance(bubble_up); 439 | if(balance < -1) { 440 | if(_bostree_balance(bubble_up->left_child_node) > 0) { 441 | _bostree_rotate_left(tree, bubble_up->left_child_node); 442 | } 443 | bubble_up = _bostree_rotate_right(tree, bubble_up); 444 | } 445 | else if(balance > 1) { 446 | if(_bostree_balance(bubble_up->right_child_node) < 0) { 447 | _bostree_rotate_right(tree, bubble_up->right_child_node); 448 | } 449 | bubble_up = _bostree_rotate_left(tree, bubble_up); 450 | } 451 | else { 452 | if(!depth_changed) { 453 | // If we neither had to rotate nor to change the depth, 454 | // then we are obviously finished. Only update child 455 | // counts from here on. 456 | bubbling_finished = 1; 457 | } 458 | } 459 | } 460 | 461 | if(bubble_up->parent_node) { 462 | if(bubble_up->parent_node->left_child_node == bubble_up) { 463 | bubble_up->parent_node->left_child_count--; 464 | } 465 | else { 466 | bubble_up->parent_node->right_child_count--; 467 | } 468 | } 469 | bubble_up = bubble_up->parent_node; 470 | } 471 | 472 | node->weak_ref_node_valid = 0; 473 | bostree_node_weak_unref(tree, node); 474 | } 475 | 476 | BOSNode *bostree_node_weak_ref(BOSNode *node) { 477 | assert(node->weak_ref_count < 127); 478 | assert(node->weak_ref_count > 0); 479 | node->weak_ref_count++; 480 | return node; 481 | } 482 | 483 | BOSNode *bostree_node_weak_unref(BOSTree *tree, BOSNode *node) { 484 | node->weak_ref_count--; 485 | if(node->weak_ref_count == 0) { 486 | if(tree->free_function) { 487 | tree->free_function(node); 488 | } 489 | free(node); 490 | } 491 | else if(node->weak_ref_node_valid) { 492 | return node; 493 | } 494 | return NULL; 495 | } 496 | 497 | BOSNode *bostree_lookup(BOSTree *tree, const void *key) { 498 | BOSNode *node = tree->root_node; 499 | while(node) { 500 | int cmp = tree->cmp_function(key, node->key); 501 | if(cmp == 0) { 502 | break; 503 | } 504 | else if(cmp < 0) { 505 | node = node->left_child_node; 506 | } 507 | else { 508 | node = node->right_child_node; 509 | } 510 | } 511 | return node; 512 | } 513 | 514 | BOSNode *bostree_select(BOSTree *tree, unsigned int index) { 515 | BOSNode *node = tree->root_node; 516 | while(node) { 517 | if(node->left_child_count <= index) { 518 | index -= node->left_child_count; 519 | if(index == 0) { 520 | return node; 521 | } 522 | index--; 523 | node = node->right_child_node; 524 | } 525 | else { 526 | node = node->left_child_node; 527 | } 528 | } 529 | return node; 530 | } 531 | 532 | BOSNode *bostree_next_node(BOSNode *node) { 533 | if(node->right_child_node) { 534 | node = node->right_child_node; 535 | while(node->left_child_node) { 536 | node = node->left_child_node; 537 | } 538 | return node; 539 | } 540 | else if(node->parent_node) { 541 | while(node->parent_node && node->parent_node->right_child_node == node) { 542 | node = node->parent_node; 543 | } 544 | return node->parent_node; 545 | } 546 | return NULL; 547 | } 548 | 549 | BOSNode *bostree_previous_node(BOSNode *node) { 550 | if(node->left_child_node) { 551 | node = node->left_child_node; 552 | while(node->right_child_node) { 553 | node = node->right_child_node; 554 | } 555 | return node; 556 | } 557 | else if(node->parent_node) { 558 | while(node->parent_node && node->parent_node->left_child_node == node) { 559 | node = node->parent_node; 560 | } 561 | return node->parent_node; 562 | } 563 | return NULL; 564 | } 565 | 566 | unsigned int bostree_rank(BOSNode *node) { 567 | unsigned int counter = node->left_child_count; 568 | while(node) { 569 | if(node->parent_node && node->parent_node->right_child_node == node) counter += 1 + node->parent_node->left_child_count; 570 | node = node->parent_node; 571 | } 572 | return counter; 573 | } 574 | 575 | #if !defined(NDEBUG) 576 | #include 577 | #include 578 | 579 | /* Debug helpers: 580 | 581 | Print the tree to stdout in dot format. 582 | */ 583 | 584 | static void _bostree_print_helper(BOSNode *node) { 585 | printf(" %s [label=\"\\N (%d,%d,%d)\"];\n", (char *)node->key, node->left_child_count, node->right_child_count, node->depth); 586 | if(node->parent_node) { 587 | printf(" %s -> %s [color=green];\n", (char *)node->key, (char *)node->parent_node->key); 588 | } 589 | 590 | if(node->left_child_node != NULL) { 591 | printf(" %s -> %s\n", (char *)node->key, (char *)node->left_child_node->key); 592 | _bostree_print_helper(node->left_child_node); 593 | } 594 | if(node->right_child_node != NULL) { 595 | printf(" %s -> %s\n", (char *)node->key, (char *)node->right_child_node->key); 596 | _bostree_print_helper(node->right_child_node); 597 | } 598 | } 599 | 600 | void bostree_print(BOSTree *tree) { 601 | if(tree->root_node == NULL) { 602 | return; 603 | } 604 | 605 | printf("digraph {\n ordering = out;\n"); 606 | _bostree_print_helper(tree->root_node); 607 | printf("}\n"); 608 | fsync(0); 609 | } 610 | #endif 611 | -------------------------------------------------------------------------------- /lib/thumbnailcache.c: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of pqiv 3 | * Copyright (c) 2017, Phillip Berndt 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * You should have received a copy of the GNU General Public License 14 | * along with this program. If not, see . 15 | * 16 | * This implements thumbnail caching as specified in 17 | * https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html 18 | */ 19 | 20 | #ifndef CONFIGURED_WITHOUT_MONTAGE_MODE 21 | 22 | #include "thumbnailcache.h" 23 | 24 | #ifdef _WIN32 25 | #include 26 | #else 27 | #include 28 | #endif 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | #include 39 | #if !GLIB_CHECK_VERSION(2, 36, 0) 40 | #define g_close(fd, errptr) close(fd) 41 | #endif 42 | #include 43 | #include 44 | 45 | // Unavailability of an mtime is usually fatal, but if the thumb was created 46 | // with an absent mtime, this is tolerable (and actually beneficial, because it 47 | // enables montage mode on incomplete data). 48 | // 49 | // This does not get any special treatment at checking time -- if 50 | // 1970-01-01T00:00:00 is indeed a value placed in the MTime of a thumbnail, 51 | // chances are that the creator might even have intended to actually not set 52 | // the MTime. 53 | const time_t MTIME_UNAVAILABLE = 0; 54 | 55 | /* Sorted in decreasing size, such that when looking for thumbnails of at least 56 | * some size, they can be traversed last-to-first, starting at the one that 57 | * first has a chance to match (see use of minimum_level_index). */ 58 | static const char * thumbnail_levels[] = { "x-pqiv", "xx-large", "x-large", "large", "normal" }; 59 | 60 | /* CRC calculation as per PNG TR, Annex D */ 61 | static unsigned long crc_table[256]; 62 | static gboolean crc_table_computed = 0; 63 | static void make_crc_table(void) { 64 | unsigned long c; 65 | int n, k; 66 | 67 | for(n = 0; n < 256; n++) { 68 | c = (unsigned long) n; 69 | for(k = 0; k < 8; k++) { 70 | if(c & 1) { 71 | c = 0xedb88320L ^ (c >> 1); 72 | } 73 | else { 74 | c >>= 1; 75 | } 76 | } 77 | crc_table[n] = c; 78 | } 79 | crc_table_computed = 1; 80 | } 81 | static unsigned long crc(unsigned long crc, unsigned char *buf, int len) { 82 | unsigned long c = crc ^ 0xffffffffL; 83 | int n; 84 | 85 | if (!crc_table_computed) { 86 | make_crc_table(); 87 | } 88 | for (n = 0; n < len; n++) { 89 | c = crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8); 90 | } 91 | return c ^ 0xffffffffL; 92 | } 93 | 94 | /* Auxiliary functions */ 95 | static gchar *get_local_filename(file_t *file) { 96 | // Memory files do not have a file name 97 | if(file->file_flags & FILE_FLAGS_MEMORY_IMAGE) { 98 | return NULL; 99 | } 100 | 101 | // Retrieve file name 102 | GFile *gfile = gfile_for_commandline_arg(file->file_name); 103 | gchar *file_path = g_file_get_path(gfile); 104 | g_object_unref(gfile); 105 | return file_path; 106 | } 107 | 108 | static const gchar *get_multi_page_suffix(file_t *file) { 109 | // Multi-page documents do not have an unambigous file name 110 | // Since the Thumbnail Managing Standard does not state how to format an 111 | // URI into e.g. an archive, we need to make something up. Do not use 112 | // the standard directories in this case. 113 | gchar *display_basename = g_strrstr(file->display_name, G_DIR_SEPARATOR_S); 114 | if(display_basename) { 115 | display_basename++; 116 | } 117 | else { 118 | display_basename = file->display_name; 119 | } 120 | gchar *filename_basename = g_strrstr(file->file_name, G_DIR_SEPARATOR_S); 121 | if(filename_basename) { 122 | filename_basename++; 123 | } 124 | else { 125 | filename_basename = file->file_name; 126 | } 127 | 128 | int filename_basename_length = strlen(filename_basename); 129 | int display_basename_length = strlen(display_basename); 130 | 131 | if(filename_basename_length == display_basename_length) { 132 | return NULL; 133 | } 134 | 135 | return display_basename + filename_basename_length; 136 | } 137 | 138 | static gchar *_local_thumbnail_cache_directory; 139 | static const gchar *get_thumbnail_cache_directory() { 140 | if(!_local_thumbnail_cache_directory) { 141 | const gchar *cache_dir = g_getenv("XDG_CACHE_HOME"); 142 | if(!cache_dir) { 143 | _local_thumbnail_cache_directory = g_build_filename(g_getenv("HOME"), ".cache", "thumbnails", NULL); 144 | } 145 | else { 146 | _local_thumbnail_cache_directory = g_build_filename(cache_dir, "thumbnails", NULL); 147 | } 148 | } 149 | return _local_thumbnail_cache_directory; 150 | } 151 | 152 | gboolean check_png_attributes(gchar *file_name, gchar *file_uri, time_t file_mtime) { 153 | // Parse PNG headers and check whether the Thumb::URI and Thumb::MTime 154 | // headers are up to date. 155 | // 156 | // See below in png_writer for a rough explaination, or read the PNG TR 157 | // https://www.w3.org/TR/PNG/ 158 | 159 | // Tracking whether they were found to return early once both are matched. 160 | gboolean found_uri = FALSE; 161 | gboolean found_mtime = FALSE; 162 | gboolean found_mismatch = FALSE; 163 | 164 | int fd = g_open(file_name, O_RDONLY, 0); 165 | if(fd < 0) { 166 | return FALSE; 167 | } 168 | 169 | union { 170 | char buf[8]; 171 | uint32_t uint32; 172 | } header; 173 | 174 | // File header 175 | if(read(fd, header.buf, 8) != 8) { 176 | g_close(fd, NULL); 177 | return FALSE; 178 | } 179 | const unsigned char expected_header[] = { 137, 80, 78, 71, 13, 10, 26, 10 }; 180 | if(memcmp(header.buf, expected_header, sizeof(expected_header)) != 0) { 181 | g_close(fd, NULL); 182 | return FALSE; 183 | } 184 | 185 | // Read all chunks until we have both matches 186 | while(1) { 187 | if(read(fd, header.buf, 8) != 8) { 188 | g_close(fd, NULL); 189 | return !found_mismatch; 190 | } 191 | 192 | int header_length = (int)ntohl(header.uint32); 193 | if(header_length < 0) { 194 | // While technically, this is allowed, no header should ever be 195 | // this large. 196 | g_close(fd, NULL); 197 | return FALSE; 198 | } 199 | 200 | if(strncmp(&header.buf[4], "tEXt", 4) == 0) { 201 | // This is interesting. Read the whole contents first. 202 | char *data = g_malloc(header_length); 203 | if(read(fd, data, header_length) != header_length) { 204 | g_free(data); 205 | g_close(fd, NULL); 206 | return FALSE; 207 | } 208 | 209 | // Check against CRC 210 | if(read(fd, header.buf, 4) != 4) { 211 | g_free(data); 212 | g_close(fd, NULL); 213 | return FALSE; 214 | } 215 | unsigned file_crc = ntohl(header.uint32); 216 | unsigned actual_crc = crc(crc(0, (unsigned char*)"tEXt", 4), (unsigned char *)data, header_length); 217 | 218 | if(file_crc == actual_crc) { 219 | if(strcmp(data, "Thumb::URI") == 0) { 220 | gboolean match = strncmp(&data[sizeof("Thumb::URI")], file_uri, strlen(file_uri)) == 0; 221 | found_mismatch |= !match; 222 | found_uri = TRUE; 223 | } 224 | else if(strcmp(data, "Thumb::MTime") == 0) { 225 | gchar *file_mtime_str = g_strdup_printf("%" PRIuMAX, (intmax_t)file_mtime); 226 | gboolean match = strncmp(&data[sizeof("Thumb::MTime")], file_mtime_str, strlen(file_mtime_str)) == 0; 227 | g_free(file_mtime_str); 228 | found_mismatch |= !match; 229 | found_mtime = TRUE; 230 | } 231 | 232 | if((found_uri && found_mtime) || found_mismatch) { 233 | g_free(data); 234 | g_close(fd, NULL); 235 | return !found_mismatch; 236 | } 237 | } 238 | 239 | g_free(data); 240 | } 241 | else { 242 | // Skip header and its CRC 243 | if(lseek(fd, header_length + 4, SEEK_CUR) < 0) { 244 | g_close(fd, NULL); 245 | return FALSE; 246 | } 247 | } 248 | } 249 | } 250 | 251 | static cairo_surface_t *load_thumbnail(gchar *file_name, gchar *file_uri, time_t file_mtime, unsigned width, unsigned height) { 252 | // Check if the file is up to date 253 | if(!check_png_attributes(file_name, file_uri, file_mtime)) { 254 | return NULL; 255 | } 256 | 257 | cairo_surface_t *thumbnail = cairo_image_surface_create_from_png(file_name); 258 | if(cairo_surface_status(thumbnail) != CAIRO_STATUS_SUCCESS) { 259 | cairo_surface_destroy(thumbnail); 260 | return NULL; 261 | } 262 | 263 | unsigned actual_width = cairo_image_surface_get_width(thumbnail); 264 | unsigned actual_height = cairo_image_surface_get_height(thumbnail); 265 | 266 | if(actual_width == width || actual_height == height) { 267 | return thumbnail; 268 | } 269 | 270 | if(actual_width < width && actual_height < height) { 271 | // Can't use this. Too small. 272 | cairo_surface_destroy(thumbnail); 273 | return NULL; 274 | } 275 | 276 | double scale_factor = fmin(1., fmin(width * 1. / actual_width, height * 1. / actual_height)); 277 | unsigned target_width = actual_width * scale_factor; 278 | unsigned target_height = actual_height * scale_factor; 279 | 280 | #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 12, 0) 281 | cairo_surface_t *target_thumbnail = cairo_surface_create_similar_image(thumbnail, CAIRO_FORMAT_ARGB32, target_width, target_height); 282 | #else 283 | cairo_surface_t *target_thumbnail = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, target_width, target_height); 284 | #endif 285 | 286 | cairo_t *cr = cairo_create(target_thumbnail); 287 | cairo_scale(cr, scale_factor, scale_factor); 288 | cairo_set_source_surface(cr, thumbnail, 0, 0); 289 | cairo_paint(cr); 290 | cairo_destroy(cr); 291 | 292 | cairo_surface_destroy(thumbnail); 293 | thumbnail = target_thumbnail; 294 | if(cairo_surface_status(thumbnail) != CAIRO_STATUS_SUCCESS) { 295 | cairo_surface_destroy(thumbnail); 296 | return NULL; 297 | } 298 | 299 | return thumbnail; 300 | } 301 | 302 | /* This library's public API */ 303 | gboolean load_thumbnail_from_cache(file_t *file, unsigned width, unsigned height, thumbnail_persist_mode_t persist_mode, char *special_thumbnail_directory) { 304 | if(persist_mode == THUMBNAILS_PERSIST_OFF) { 305 | return FALSE; 306 | } 307 | 308 | // Obtain a local path to the file 309 | gchar *local_filename = get_local_filename(file); 310 | if(!local_filename) { 311 | return FALSE; 312 | } 313 | const gchar *multi_page_suffix = get_multi_page_suffix(file); 314 | 315 | // Obtain modification timestamp 316 | struct stat file_stat; 317 | time_t file_mtime; 318 | if(stat(local_filename, &file_stat) < 0) { 319 | file_mtime = MTIME_UNAVAILABLE; 320 | } else { 321 | file_mtime = file_stat.st_mtime; 322 | } 323 | 324 | // Obtain the name of the candidate for the local thumbnail file 325 | gchar *file_uri = multi_page_suffix ? g_strdup_printf("file://%s#%s", local_filename, multi_page_suffix) : g_strdup_printf("file://%s", local_filename); 326 | gchar *md5_filename = g_compute_checksum_for_string(G_CHECKSUM_MD5, file_uri, -1); 327 | 328 | unsigned int minimum_level_index = 0; 329 | if (!multi_page_suffix) { 330 | if (width <= 128 && height <= 128) { 331 | minimum_level_index = 4; 332 | } else if (width <= 256 && height <= 256) { 333 | minimum_level_index = 3; 334 | } else if (width <= 512 && height <= 512) { 335 | minimum_level_index = 2; 336 | } else if (width <= 1024 && height <= 1024) { 337 | minimum_level_index = 1; 338 | } else { 339 | minimum_level_index = 0; 340 | } 341 | } 342 | 343 | // Search two directory structures: special_thumbnail_directory, then get_thumbnail_cache_directory() 344 | for(int k=0; k<2; k++) { 345 | if(k == 0 && special_thumbnail_directory == NULL) { 346 | continue; 347 | } 348 | // Search in the directories for the different sizes 349 | for(int j=minimum_level_index; j>=0; j--) { 350 | gchar *thumbnail_candidate; 351 | if(j == 0) { 352 | thumbnail_candidate = g_strdup_printf("%s%s%s%s%dx%d%s%s.png", k == 0 ? special_thumbnail_directory : get_thumbnail_cache_directory(), G_DIR_SEPARATOR_S, thumbnail_levels[j], G_DIR_SEPARATOR_S, width, height, G_DIR_SEPARATOR_S, md5_filename); 353 | } 354 | else { 355 | thumbnail_candidate = g_strdup_printf("%s%s%s%s%s.png", k == 0 ? special_thumbnail_directory : get_thumbnail_cache_directory(), G_DIR_SEPARATOR_S, thumbnail_levels[j], G_DIR_SEPARATOR_S, md5_filename); 356 | } 357 | if(g_file_test(thumbnail_candidate, G_FILE_TEST_EXISTS)) { 358 | cairo_surface_t *thumbnail = load_thumbnail(thumbnail_candidate, file_uri, file_mtime, width, height); 359 | g_free(thumbnail_candidate); 360 | if(thumbnail != NULL) { 361 | file->thumbnail = thumbnail; 362 | g_free(local_filename); 363 | g_free(file_uri); 364 | g_free(md5_filename); 365 | return TRUE; 366 | } 367 | } 368 | else { 369 | g_free(thumbnail_candidate); 370 | } 371 | } 372 | } 373 | 374 | g_free(file_uri); 375 | g_free(md5_filename); 376 | 377 | // Check if a shared thumbnail directory exists and try to load from there 378 | gchar *file_dirname = g_path_get_dirname(local_filename); 379 | gchar *shared_thumbnail_directory = g_build_filename(file_dirname, ".sh_thumbnails", NULL); 380 | g_free(file_dirname); 381 | if(g_file_test(shared_thumbnail_directory, G_FILE_TEST_IS_DIR)) { 382 | gchar *file_basename = g_path_get_basename(local_filename); 383 | if(multi_page_suffix) { 384 | gchar *new_basename = g_strdup_printf("%s#%s", file_basename, multi_page_suffix); 385 | g_free(file_basename); 386 | file_basename = new_basename; 387 | } 388 | gchar *md5_basename = g_compute_checksum_for_string(G_CHECKSUM_MD5, file_basename, -1); 389 | 390 | for(int j=minimum_level_index; j>=0; j--) { 391 | gchar *thumbnail_candidate; 392 | if(j == 0) { 393 | thumbnail_candidate = g_strdup_printf("%s%s%s%s%dx%d%s%s.png", shared_thumbnail_directory, G_DIR_SEPARATOR_S, thumbnail_levels[j], G_DIR_SEPARATOR_S, width, height, G_DIR_SEPARATOR_S, md5_basename); 394 | } 395 | else { 396 | thumbnail_candidate = g_strdup_printf("%s%s%s%s%s.png", shared_thumbnail_directory, G_DIR_SEPARATOR_S, thumbnail_levels[j], G_DIR_SEPARATOR_S, md5_basename); 397 | } 398 | if(g_file_test(thumbnail_candidate, G_FILE_TEST_EXISTS)) { 399 | cairo_surface_t *thumbnail = load_thumbnail(thumbnail_candidate, file_basename, file_mtime, width, height); 400 | g_free(thumbnail_candidate); 401 | if(thumbnail != NULL) { 402 | file->thumbnail = thumbnail; 403 | g_free(md5_basename); 404 | g_free(local_filename); 405 | g_free(shared_thumbnail_directory); 406 | return TRUE; 407 | } 408 | } 409 | else { 410 | g_free(thumbnail_candidate); 411 | } 412 | } 413 | 414 | g_free(md5_basename); 415 | g_free(file_basename); 416 | } 417 | g_free(shared_thumbnail_directory); 418 | g_free(local_filename); 419 | 420 | return FALSE; 421 | } 422 | 423 | struct png_writer_info { 424 | int output_file_fd; 425 | size_t bytes_written; 426 | gchar *Thumb_URI; 427 | gchar *Thumb_MTime; 428 | }; 429 | 430 | static cairo_status_t png_writer(struct png_writer_info *info, const unsigned char *data, unsigned int length) { 431 | // This is actually quite simple: A PNG file always begins with the bytes 432 | // (137, 80, 78, 71, 13, 10, 26, 10), followed by chunks, which are 433 | // 4 bytes payload length, 4 bytes (ASCII) type, payload, and 4 bytes CRC 434 | // as defined above, taken over type & payload. 435 | // We want to inject a chunk of type tEXt, whose payload is key\0value, 436 | // after the IHDR header, which does always come first, is required, and 437 | // has fixed length 13. 438 | // 439 | const unsigned inject_pos = 8 /* header */ + (4 + 4 + 4 + 13) /* IHDR */; 440 | if(info->bytes_written < inject_pos && info->bytes_written + length >= inject_pos) { 441 | ssize_t result = write(info->output_file_fd, data, inject_pos - info->bytes_written); 442 | if(result < 0 || (size_t)result != inject_pos - info->bytes_written) { 443 | return CAIRO_STATUS_WRITE_ERROR; 444 | } 445 | data += inject_pos - info->bytes_written; 446 | length -= inject_pos - info->bytes_written; 447 | info->bytes_written = inject_pos; 448 | 449 | int uri_length = strlen(info->Thumb_URI); 450 | int output_length = 4 + 4 + sizeof("Thumb::URI") + uri_length + 4; 451 | 452 | 453 | char *output = g_malloc(output_length); 454 | uint32_t write_uint32 = htonl(sizeof("Thumb::URI") + uri_length); 455 | memcpy(output, &write_uint32, sizeof(uint32_t)); 456 | strcpy(&output[4], "tEXtThumb::URI"); 457 | strcpy(&output[19], info->Thumb_URI); 458 | write_uint32 = htonl(crc(0, (unsigned char*)&output[4], 19 + uri_length - 4)); 459 | memcpy(&output[19+uri_length] , &write_uint32, sizeof(uint32_t)); 460 | if(write(info->output_file_fd, output, output_length) != output_length) { 461 | return CAIRO_STATUS_WRITE_ERROR; 462 | } 463 | g_free(output); 464 | 465 | int mtime_length = strlen(info->Thumb_MTime); 466 | output_length = 4 + 4 + sizeof("Thumb::MTime") + mtime_length + 4; 467 | output = g_malloc(output_length); 468 | write_uint32 = htonl(sizeof("Thumb::MTime") + mtime_length); 469 | memcpy(output, &write_uint32, sizeof(uint32_t)); 470 | strcpy(&output[4], "tEXtThumb::MTime"); 471 | strcpy(&output[21], info->Thumb_MTime); 472 | write_uint32 = htonl(crc(0, (unsigned char*)&output[4], 21 + mtime_length - 4)); 473 | memcpy(&output[21+mtime_length], &write_uint32, sizeof(uint32_t)); 474 | if(write(info->output_file_fd, output, output_length) != output_length) { 475 | return CAIRO_STATUS_WRITE_ERROR; 476 | } 477 | g_free(output); 478 | } 479 | 480 | if(length > 0) { 481 | ssize_t result = write(info->output_file_fd, data, length); 482 | if(result < 0 || (unsigned int)result != length) { 483 | return CAIRO_STATUS_WRITE_ERROR; 484 | } 485 | info->bytes_written += length; 486 | } 487 | 488 | return CAIRO_STATUS_SUCCESS; 489 | } 490 | 491 | gboolean store_thumbnail_to_cache(file_t *file, unsigned width, unsigned height, thumbnail_persist_mode_t persist_mode, char *special_thumbnail_directory) { 492 | if(persist_mode == THUMBNAILS_PERSIST_OFF || persist_mode == THUMBNAILS_PERSIST_RO) { 493 | return FALSE; 494 | } 495 | 496 | // We only store thumbnails if they have the correct size 497 | unsigned actual_width = cairo_image_surface_get_width(file->thumbnail); 498 | unsigned actual_height = cairo_image_surface_get_height(file->thumbnail); 499 | int thumbnail_level; 500 | 501 | // If the file didn't need thumbnailing, don't store a thumbnail either. 502 | // This is a simple way to make sure that we don't accidentally thumbnail 503 | // thumbnails again. 504 | if(actual_width == file->width || actual_height == file->height) { 505 | return FALSE; 506 | } 507 | 508 | if(width == 256 && height == 256) { 509 | thumbnail_level = 3; 510 | } 511 | else if(width == 128 && height == 128) { 512 | thumbnail_level = 4; 513 | } 514 | else if(width == 512 && height == 512) { 515 | thumbnail_level = 2; 516 | } 517 | else if(width == 1024 && height == 1024) { 518 | thumbnail_level = 1; 519 | } 520 | else { 521 | thumbnail_level = 0; 522 | if(persist_mode == THUMBNAILS_PERSIST_STANDARD) { 523 | return FALSE; 524 | } 525 | } 526 | 527 | // Obtain absolute path to file 528 | gchar *local_filename = get_local_filename(file); 529 | if(!local_filename) { 530 | return FALSE; 531 | } 532 | const gchar *multi_page_suffix = get_multi_page_suffix(file); 533 | if(multi_page_suffix) { 534 | // Unspecified by standard, use x-pqiv cache 535 | thumbnail_level = 0; 536 | if(persist_mode == THUMBNAILS_PERSIST_STANDARD) { 537 | g_free(local_filename); 538 | return FALSE; 539 | } 540 | } 541 | 542 | // Obtain modification timestamp 543 | struct stat file_stat; 544 | if(stat(local_filename, &file_stat) < 0) { 545 | g_free(local_filename); 546 | return FALSE; 547 | } 548 | time_t file_mtime = file_stat.st_mtime; 549 | 550 | // Obtain the name of the thumbnail file 551 | gchar *file_uri, *md5_filename, *thumbnail_directory, *thumbnail_file; 552 | if(persist_mode == THUMBNAILS_PERSIST_LOCAL) { 553 | // Create a .sh_thumbnails directory 554 | file_uri = g_path_get_basename(local_filename); 555 | if(multi_page_suffix) { 556 | gchar *new_uri = g_strdup_printf("%s#%s", file_uri, multi_page_suffix); 557 | g_free(file_uri); 558 | file_uri = new_uri; 559 | } 560 | md5_filename = g_compute_checksum_for_string(G_CHECKSUM_MD5, file_uri, -1); 561 | gchar *file_dirname = g_path_get_dirname(local_filename); 562 | if(thumbnail_level == 0) { 563 | thumbnail_directory = g_strdup_printf("%s%s.sh_thumbnails%s%s%s%dx%d", file_dirname, G_DIR_SEPARATOR_S, G_DIR_SEPARATOR_S, thumbnail_levels[0], G_DIR_SEPARATOR_S, width, height); 564 | } 565 | else { 566 | thumbnail_directory = g_strdup_printf("%s%s.sh_thumbnails%s%s", file_dirname, G_DIR_SEPARATOR_S, G_DIR_SEPARATOR_S, thumbnail_levels[thumbnail_level]); 567 | } 568 | g_free(file_dirname); 569 | thumbnail_file = g_strdup_printf("%s%s%s.png", thumbnail_directory, G_DIR_SEPARATOR_S, md5_filename); 570 | } 571 | else { 572 | // Use the standardized cache format, possibly with special directory 573 | if(multi_page_suffix) { 574 | file_uri = g_strdup_printf("file://%s#%s", local_filename, multi_page_suffix); 575 | } 576 | else { 577 | file_uri = g_strdup_printf("file://%s", local_filename); 578 | } 579 | md5_filename = g_compute_checksum_for_string(G_CHECKSUM_MD5, file_uri, -1); 580 | if(thumbnail_level == 0) { 581 | thumbnail_directory = g_strdup_printf("%s%s%s%s%dx%d", special_thumbnail_directory ? special_thumbnail_directory : get_thumbnail_cache_directory(), G_DIR_SEPARATOR_S, thumbnail_levels[thumbnail_level], G_DIR_SEPARATOR_S, width, height); 582 | } 583 | else { 584 | thumbnail_directory = g_strdup_printf("%s%s%s", special_thumbnail_directory ? special_thumbnail_directory : get_thumbnail_cache_directory(), G_DIR_SEPARATOR_S, thumbnail_levels[thumbnail_level]); 585 | } 586 | thumbnail_file = g_strdup_printf("%s%s%s.png", thumbnail_directory, G_DIR_SEPARATOR_S, md5_filename); 587 | } 588 | 589 | // Create the directory if necessary 590 | if(!g_file_test(thumbnail_directory, G_FILE_TEST_IS_DIR)) { 591 | g_mkdir_with_parents(thumbnail_directory, 0700); 592 | } 593 | g_free(thumbnail_directory); 594 | 595 | // Write out thumbnail 596 | // We use a wrapper to inject the tEXt chunks as required by the thumbnail standard 597 | gboolean retval = TRUE; 598 | int file_fd = g_open(thumbnail_file, O_CREAT | O_WRONLY, 0600); 599 | if(file_fd >= 0) { 600 | gchar *string_mtime = g_strdup_printf("%" PRIuMAX, (intmax_t)file_mtime); 601 | struct png_writer_info writer_info = { file_fd, 0, file_uri, string_mtime }; 602 | if(cairo_surface_write_to_png_stream(file->thumbnail, (cairo_write_func_t)png_writer, &writer_info) != CAIRO_STATUS_SUCCESS) { 603 | g_unlink(thumbnail_file); 604 | retval = FALSE; 605 | } 606 | g_free(string_mtime); 607 | } 608 | g_close(file_fd, NULL); 609 | 610 | g_free(file_uri); 611 | g_free(md5_filename); 612 | g_free(thumbnail_file); 613 | g_free(local_filename); 614 | 615 | return retval; 616 | } 617 | 618 | #else 619 | void __thumbnailcache__empty_translation_unit() {} 620 | #endif 621 | --------------------------------------------------------------------------------