├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── animate.cpp ├── animate.hpp ├── args.cpp ├── args.hpp ├── codecs ├── ani.cpp ├── ani.hpp ├── avif.cpp ├── avif.hpp ├── binio.hpp ├── bitstream.hpp ├── bmp.cpp ├── bmp.hpp ├── bmp_common.cpp ├── bmp_common.hpp ├── bpg.cpp ├── bpg.hpp ├── exif.cpp ├── exif.hpp ├── flif.cpp ├── flif.hpp ├── gif.cpp ├── gif.hpp ├── heif.cpp ├── heif.hpp ├── ico.cpp ├── ico.hpp ├── image.cpp ├── image.hpp ├── jp2.cpp ├── jp2.hpp ├── jp2_color.cpp ├── jp2_color.hpp ├── jpeg.cpp ├── jpeg.hpp ├── jxl.cpp ├── jxl.hpp ├── mcmap.cpp ├── mcmap.hpp ├── mng.cpp ├── mng.hpp ├── motologo.cpp ├── motologo.hpp ├── openexr.cpp ├── openexr.hpp ├── pcx.cpp ├── pcx.hpp ├── pkmn_gen1.cpp ├── pkmn_gen1.hpp ├── pkmn_gen2.cpp ├── pkmn_gen2.hpp ├── png.cpp ├── png.hpp ├── pnm.cpp ├── pnm.hpp ├── sif.cpp ├── sif.hpp ├── srf.cpp ├── srf.hpp ├── sub_args.cpp ├── sub_args.hpp ├── svg.cpp ├── svg.hpp ├── tga.cpp ├── tga.hpp ├── tiff.cpp ├── tiff.hpp ├── webp.cpp ├── webp.hpp ├── xpm.cpp └── xpm.hpp ├── color.hpp ├── config.h.in ├── cxxopts_wrapper.hpp ├── display.cpp ├── display.hpp ├── font.cpp ├── font.hpp └── main.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | asciiart 2 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.8 FATAL_ERROR) 2 | project(asciiart CXX) 3 | 4 | set(CMAKE_CXX_STANDARD 20) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | add_compile_options(-Wall -Wextra) 7 | 8 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) 9 | 10 | if(NOT CMAKE_BUILD_TYPE) 11 | set(CMAKE_BUILD_TYPE "Release") 12 | endif() 13 | 14 | find_package(cxxopts REQUIRED) 15 | find_package(PkgConfig REQUIRED) 16 | find_package(libavif QUIET) 17 | if(libavif_FOUND) 18 | message(STATUS "Found libavif version ${libavif_VERSION}") # TODO: is there a cleaner way to do this? 19 | set(AVIF_FOUND ${libavif_FOUND}) 20 | endif() 21 | find_package(GIF QUIET) 22 | find_package(JPEG QUIET) 23 | find_package(PNG QUIET) 24 | find_package(TIFF QUIET) 25 | find_package(ZLIB QUIET) 26 | pkg_check_modules(EXIF libexif) 27 | pkg_check_modules(HEIF libheif) 28 | pkg_check_modules(JP2 libopenjp2) 29 | pkg_check_modules(JXL libjxl) 30 | pkg_check_modules(MNG libmng) 31 | pkg_check_modules(OpenEXR OpenEXR) 32 | pkg_check_modules(SVG librsvg-2.0) 33 | pkg_check_modules(WEBP libwebp) 34 | pkg_check_modules(XPM xpm) 35 | 36 | include(CheckIncludeFiles) 37 | check_include_files(signal.h HAS_SIGNAL) 38 | check_include_files(sys/ioctl.h HAS_IOCTL) 39 | check_include_files(sys/select.h HAS_SELECT) 40 | check_include_files(termios.h HAS_TERMIOS) 41 | check_include_files(unistd.h HAS_UNISTD) 42 | 43 | if(WIN32) 44 | check_include_files(windows.h HAS_WINDOWS) 45 | endif() 46 | 47 | check_include_files(libbpg.h BPG_FOUND) 48 | check_include_files(flif.h FLIF_ENC_FOUND) 49 | check_include_files(flif_dec.h FLIF_DEC_FOUND) 50 | 51 | add_executable(asciiart 52 | animate.cpp 53 | args.cpp 54 | display.cpp 55 | font.cpp 56 | main.cpp 57 | codecs/image.cpp 58 | codecs/sub_args.cpp 59 | codecs/ani.cpp 60 | codecs/bmp.cpp 61 | codecs/bmp_common.cpp 62 | codecs/ico.cpp 63 | codecs/motologo.cpp 64 | codecs/pcx.cpp 65 | codecs/pkmn_gen1.cpp 66 | codecs/pkmn_gen2.cpp 67 | codecs/pnm.cpp 68 | codecs/sif.cpp 69 | codecs/srf.cpp 70 | codecs/tga.cpp 71 | ) 72 | 73 | target_include_directories(asciiart PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) 74 | 75 | find_package(Freetype QUIET) 76 | if(FREETYPE_FOUND) 77 | pkg_check_modules(FONTCONFIG fontconfig) 78 | if(FONTCONFIG_FOUND) 79 | target_include_directories(asciiart PRIVATE 80 | ${FREETYPE_INCLUDE_DIRS} 81 | ${FONTCONFIG_INCLUDE_DIRS} 82 | ) 83 | 84 | target_link_libraries(asciiart 85 | ${FREETYPE_LIBRARIES} 86 | ${FONTCONFIG_LIBRARIES} 87 | ) 88 | endif() 89 | endif() 90 | 91 | configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h) 92 | 93 | if(AVIF_FOUND) 94 | target_sources(asciiart PRIVATE codecs/avif.cpp) 95 | target_link_libraries(asciiart avif) 96 | endif() 97 | if(BPG_FOUND) 98 | target_sources(asciiart PRIVATE codecs/bpg.cpp) 99 | target_link_libraries(asciiart bpg) 100 | endif() 101 | if(EXIF_FOUND) 102 | target_sources(asciiart PRIVATE codecs/exif.cpp) 103 | target_include_directories(asciiart PRIVATE ${EXIF_INCLUDE_DIRS}) 104 | target_link_libraries(asciiart ${EXIF_LIBRARIES}) 105 | endif() 106 | if(FLIF_ENC_FOUND OR FLIF_DEC_FOUND) 107 | target_sources(asciiart PRIVATE codecs/flif.cpp) 108 | if(FLIF_ENC_FOUND) 109 | target_link_libraries(asciiart flif) 110 | else() 111 | target_link_libraries(asciiart flif_dec) 112 | endif() 113 | endif() 114 | if(GIF_FOUND) 115 | target_sources(asciiart PRIVATE codecs/gif.cpp) 116 | target_include_directories(asciiart PRIVATE ${GIF_INCLUDE_DIRS}) 117 | target_link_libraries(asciiart ${GIF_LIBRARIES}) 118 | endif() 119 | if(HEIF_FOUND) 120 | target_sources(asciiart PRIVATE codecs/heif.cpp) 121 | target_include_directories(asciiart PRIVATE ${HEIF_INCLUDE_DIRS}) 122 | target_link_libraries(asciiart ${HEIF_LIBRARIES}) 123 | endif() 124 | if(JP2_FOUND) 125 | target_sources(asciiart PRIVATE codecs/jp2.cpp codecs/jp2_color.cpp) 126 | target_include_directories(asciiart PRIVATE ${JP2_INCLUDE_DIRS}) 127 | target_link_libraries(asciiart ${JP2_LIBRARIES}) 128 | endif() 129 | if(JPEG_FOUND) 130 | target_sources(asciiart PRIVATE codecs/jpeg.cpp) 131 | target_include_directories(asciiart PRIVATE ${JPEG_INCLUDE_DIRS}) 132 | target_link_libraries(asciiart ${JPEG_LIBRARIES}) 133 | endif() 134 | if(JXL_FOUND) 135 | target_sources(asciiart PRIVATE codecs/jxl.cpp) 136 | target_include_directories(asciiart PRIVATE ${JXL_INCLUDE_DIRS}) 137 | target_link_libraries(asciiart ${JXL_LIBRARIES}) 138 | endif() 139 | if(MNG_FOUND) 140 | target_sources(asciiart PRIVATE codecs/mng.cpp) 141 | target_include_directories(asciiart PRIVATE ${MNG_INCLUDE_DIRS}) 142 | target_link_libraries(asciiart ${MNG_LIBRARIES}) 143 | endif() 144 | if(OpenEXR_FOUND) 145 | target_sources(asciiart PRIVATE codecs/openexr.cpp) 146 | target_include_directories(asciiart SYSTEM PRIVATE ${OpenEXR_INCLUDE_DIRS}) # SYSTEM to suppress a bunch of warnings 147 | target_link_libraries(asciiart ${OpenEXR_LIBRARIES}) 148 | endif() 149 | if(PNG_FOUND) 150 | target_sources(asciiart PRIVATE codecs/png.cpp) 151 | target_include_directories(asciiart PRIVATE ${PNG_INCLUDE_DIRS}) 152 | target_link_libraries(asciiart ${PNG_LIBRARIES}) 153 | endif() 154 | if(SVG_FOUND) 155 | target_sources(asciiart PRIVATE codecs/svg.cpp) 156 | target_include_directories(asciiart PRIVATE ${SVG_INCLUDE_DIRS}) 157 | target_link_libraries(asciiart ${SVG_LIBRARIES}) 158 | endif() 159 | if(TIFF_FOUND) 160 | target_sources(asciiart PRIVATE codecs/tiff.cpp) 161 | target_include_directories(asciiart PRIVATE ${TIFF_INCLUDE_DIRS}) 162 | target_link_libraries(asciiart ${TIFF_LIBRARIES}) 163 | endif() 164 | if(WEBP_FOUND) 165 | target_sources(asciiart PRIVATE codecs/webp.cpp) 166 | target_include_directories(asciiart PRIVATE ${WEBP_INCLUDE_DIRS}) 167 | target_link_libraries(asciiart ${WEBP_LIBRARIES}) 168 | endif() 169 | if(XPM_FOUND) 170 | target_sources(asciiart PRIVATE codecs/xpm.cpp) 171 | target_include_directories(asciiart PRIVATE ${XPM_INCLUDE_DIRS}) 172 | target_link_libraries(asciiart ${XPM_LIBRARIES}) 173 | endif() 174 | if(ZLIB_FOUND) 175 | target_sources(asciiart PRIVATE codecs/mcmap.cpp) 176 | target_include_directories(asciiart PRIVATE ${ZLIB_INCLUDE_DIRS}) 177 | target_link_libraries(asciiart ${ZLIB_LIBRARIES}) 178 | endif() 179 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 Matthew Chandler 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASCIIart 2 | 3 | Display an image in the terminal using ANSI colors, or ASCII art. 4 | 5 | Supports a variety of image formats: 6 | 7 | * ANI 8 | * AVIF (requires libavif) 9 | * BMP 10 | * CUR / ICO 11 | * BPG (requires libbpg) 12 | * FLIF (requires libflif) 13 | * GIF (requires giflib) 14 | * HEIF / HEIC (requires libheif) 15 | * JPEG / MPF / MPO (requires libjpeg) 16 | * JPEG 2000 (requires OpenJPEG) 17 | * JPEG XL (requires libjxl) 18 | * Minecraft map items (requires zlib) 19 | * MNG / JNG (requires libmng) 20 | * Moto logo.bin files 21 | * OpenEXR (requires libopenexr) 22 | * PCX 23 | * PNG / APNG (requires libpng) 24 | * Pokemeon Gen 1 & 2 Compressed sprites 25 | * PPM / PGM / PBM / PAM / PFM 26 | * [SIF](https://adventofcode.com/2019/day/8) 27 | * SRF (Garmin GPS vehicle icon file) 28 | * SVG (requires librsvg) 29 | * TGA 30 | * TIFF (requires libtiff) 31 | * WebP (requires libwebp) 32 | * XPM (requires libxpm) 33 | 34 | Basic conversion is also supported for most formats with the `--convert` flag. 35 | Converted images are always as close to 32bit RGBA as supported by the format. 36 | Conversion can be done from any of the above formats to one of the following 37 | formats: 38 | 39 | * AVIF (requires libavif) 40 | * BMP 41 | * CUR / ICO 42 | * FLIF (requires libflif) 43 | * GIF (requires giflib) 44 | * HEIF / HEIC (requires libheif) 45 | * JPEG (requires libjpeg) 46 | * JPEG 2000 (requires OpenJPEG) 47 | * JPEG XL (requires libjxl) 48 | * Minecraft map items (requires zlib) 49 | * OpenEXR (requires libopenexr) 50 | * PCX 51 | * PNG (requires libpng) 52 | * Pokemeon Gen 1 Compressed sprites 53 | * PPM / PGM / PBM / PAM / PFM 54 | * TGA 55 | * TIFF (requires libtiff) 56 | * WebP (requires libwebp) 57 | * XPM (requires libxpm) 58 | 59 | ### TODO: 60 | 61 | Add support for 62 | 63 | * JPEG XR? XS? XT? LS? HDR? 64 | -------------------------------------------------------------------------------- /animate.cpp: -------------------------------------------------------------------------------- 1 | #include "animate.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "config.h" 14 | #include "display.hpp" 15 | 16 | #ifdef HAS_SELECT 17 | #include 18 | #endif 19 | #ifdef HAS_SIGNAL 20 | #include 21 | #endif 22 | #ifdef HAS_TERMIOS 23 | #include 24 | #endif 25 | #ifdef HAS_UNISTD 26 | #include 27 | #endif 28 | 29 | #define ESC "\x1B" 30 | #define CSI ESC "[" 31 | #define ENABLED "h" 32 | #define DISABLED "l" 33 | #define SGR "m" 34 | #define ALT_BUFF CSI "?1049" 35 | #define CURSOR CSI "?25" 36 | #define CLS CSI "2J" 37 | #define SEP ";" 38 | #define CUP "H" 39 | #define RESET_CHAR CSI "0" SGR 40 | 41 | class Animate::Animate_impl 42 | { 43 | public: 44 | explicit Animate_impl(const Args & args); 45 | ~Animate_impl(); 46 | Animate_impl(const Animate_impl &) = delete; 47 | Animate_impl & operator=(const Animate_impl &) = delete; 48 | Animate_impl(Animate_impl &&) = delete; 49 | Animate_impl & operator=(Animate_impl &&) = delete; 50 | 51 | void display(const Image & img); 52 | void set_frame_delay(std::chrono::duration delay_s); 53 | 54 | bool running() const; 55 | 56 | private: 57 | Args args_; 58 | std::chrono::duration> frame_delay_{1.0f / 30.0f}; 59 | std::chrono::high_resolution_clock::time_point last_frame_time_{std::chrono::high_resolution_clock::time_point::min()}; 60 | #ifdef HAS_TERMIOS 61 | termios old_term_info_; 62 | #endif 63 | bool running_ {true}; 64 | 65 | void open_alternate_buffer(); 66 | void close_alternate_buffer(); 67 | void set_signals(); 68 | void reset_signals(); 69 | void reset_cursor_pos() const; 70 | }; 71 | 72 | namespace 73 | { 74 | #if defined(HAS_SELECT) && defined(HAS_SIGNAL) 75 | volatile sig_atomic_t stop_flag = 0; 76 | volatile sig_atomic_t suspend_flag = 0; 77 | 78 | void handle_stop(int) { stop_flag = 1; } 79 | void handle_suspend(int) { suspend_flag = 1; } 80 | #endif 81 | 82 | void set_signal(int sig, void(*handler)(int)) 83 | { 84 | #if defined(HAS_SELECT) && defined(HAS_SIGNAL) 85 | std::string sigstr; 86 | #define CASESTR(x) case x: sigstr = #x; break; 87 | switch(sig) 88 | { 89 | CASESTR(SIGINT) 90 | CASESTR(SIGTERM) 91 | CASESTR(SIGTSTP) 92 | default: sigstr = std::to_string(sig); break; 93 | } 94 | #undef CASESTR 95 | 96 | struct sigaction action{}; 97 | 98 | if(sigaction(sig, nullptr, &action) == -1) 99 | throw std::runtime_error{std::string{"Could not get signal "} + sigstr + ": " + std::strerror(errno)}; 100 | 101 | if(!(action.sa_flags & SA_SIGINFO) && action.sa_handler == SIG_IGN) 102 | throw std::runtime_error{std::string{"Signal "} + sigstr + " is ignored"}; 103 | 104 | if(!(action.sa_flags & SA_SIGINFO) && action.sa_handler != SIG_DFL) 105 | throw std::runtime_error{std::string{"Signal "} + sigstr + " is already handled"}; 106 | 107 | sigemptyset(&action.sa_mask); 108 | action.sa_flags &= ~SA_SIGINFO; 109 | action.sa_handler = handler; 110 | 111 | if(sigaction(sig, &action, nullptr) == -1) 112 | throw std::runtime_error{std::string{"Could not set signal "} + sigstr + ": " + std::strerror(errno)}; 113 | #endif 114 | } 115 | 116 | void reset_signal(int sig) 117 | { 118 | #if defined(HAS_SELECT) && defined(HAS_SIGNAL) 119 | signal(sig, SIG_DFL); 120 | #endif 121 | } 122 | } 123 | 124 | Animate::Animate(const Args & args): 125 | pimpl{std::make_unique(args)} 126 | {} 127 | Animate::Animate_impl::Animate_impl(const Args & args): 128 | args_{args}, 129 | frame_delay_{args.animation_frame_delay > 0.0f ? args.animation_frame_delay : 1.0f / 30.0f} 130 | { 131 | #ifdef HAS_UNISTD 132 | if(!isatty(fileno(stdout))) 133 | throw std::runtime_error{"Can't animate - not a TTY"}; 134 | #endif 135 | 136 | set_signals(); 137 | open_alternate_buffer(); 138 | } 139 | 140 | Animate::~Animate() = default; // needed for unique_ptr as pimpl 141 | Animate::Animate_impl::~Animate_impl() 142 | { 143 | close_alternate_buffer(); 144 | reset_signals(); 145 | } 146 | 147 | void Animate::display(const Image & img) { pimpl->display(img); } 148 | void Animate::Animate_impl::display(const Image & img) 149 | { 150 | reset_cursor_pos(); 151 | print_image(img, args_, std::cout); 152 | std::cout.flush(); 153 | 154 | #if defined(HAS_SELECT) && defined(HAS_SIGNAL) 155 | if(suspend_flag) 156 | { 157 | close_alternate_buffer(); 158 | reset_signal(SIGTSTP); 159 | raise(SIGTSTP); 160 | 161 | set_signal(SIGTSTP, handle_suspend); 162 | open_alternate_buffer(); 163 | suspend_flag = 0; 164 | last_frame_time_ = decltype(last_frame_time_)::min(); 165 | } 166 | 167 | if(stop_flag) 168 | { 169 | running_ = false; 170 | return; 171 | } 172 | #endif 173 | 174 | auto frame_end = std::chrono::high_resolution_clock::now(); 175 | auto frame_time = std::max(decltype(frame_delay_)::zero(), std::chrono::duration_cast(frame_end-last_frame_time_)); 176 | auto sleep_time = frame_delay_ - frame_time; 177 | 178 | std::this_thread::sleep_for(sleep_time); 179 | last_frame_time_ = std::chrono::high_resolution_clock::now(); 180 | } 181 | 182 | void Animate::set_framerate(float fps) { pimpl->set_frame_delay(std::chrono::duration(1.0f / fps)); } 183 | void Animate::set_frame_delay(std::chrono::duration delay_s) { pimpl->set_frame_delay(delay_s); } 184 | void Animate::Animate_impl::set_frame_delay(std::chrono::duration delay_s) { frame_delay_ = delay_s; } 185 | 186 | bool Animate::running() const { return pimpl->running(); } 187 | bool Animate::Animate_impl::running() const { return running_; } 188 | 189 | void Animate::Animate_impl::open_alternate_buffer() 190 | { 191 | std::cout < 5 | #include 6 | 7 | #include "args.hpp" 8 | #include "codecs/image.hpp" 9 | 10 | class Animate 11 | { 12 | public: 13 | explicit Animate(const Args & args); 14 | 15 | ~Animate(); 16 | Animate(const Animate &) = delete; 17 | Animate & operator=(const Animate &) = delete; 18 | Animate(Animate &&) = delete; 19 | Animate & operator=(Animate &&) = delete; 20 | 21 | void display(const Image & img); 22 | void set_framerate(float fps); 23 | void set_frame_delay(std::chrono::duration delay_s); 24 | 25 | bool running() const; 26 | operator bool() const { return running(); } 27 | 28 | private: 29 | class Animate_impl; 30 | std::unique_ptr pimpl; 31 | }; 32 | #endif // ANIMATE_HPP 33 | -------------------------------------------------------------------------------- /args.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ARGS_HPP 2 | #define ARGS_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "config.h" 10 | 11 | struct Args 12 | { 13 | std::string input_filename; // - for stdin 14 | std::string output_filename; // - for stdout 15 | std::string font_name; // use fontconfig to find, freetype to open 16 | float font_size; // font size requested, in points 17 | std::optional rows; // output rows 18 | std::optional cols; // output cols 19 | unsigned char bg; // BG color value 20 | bool invert; // invert colors 21 | bool display; // display the image 22 | enum class Color {NONE, ANSI4, ANSI8, ANSI24} color; 23 | enum class Disp_char {HALF_BLOCK, SPACE, ASCII} disp_char; 24 | enum class Force_file 25 | { 26 | detect, // detect filetype by header 27 | pcx, 28 | tga, 29 | #ifdef SVG_FOUND 30 | svg, 31 | #endif 32 | #ifdef XPM_FOUND 33 | xpm, 34 | #endif 35 | #ifdef ZLIB_FOUND 36 | mcmap, 37 | pkmn_gen1, 38 | pkmn_gen2, 39 | #endif 40 | aoc_2019_sif, 41 | } force_file { Force_file::detect }; 42 | std::optional> convert_filename; 43 | std::optional image_no; 44 | std::optional frame_no; 45 | bool get_image_count; 46 | bool get_frame_count; 47 | bool animate; 48 | bool loop_animation; 49 | float animation_frame_delay; 50 | std::vector extra_args; 51 | std::string help_text; 52 | }; 53 | 54 | [[nodiscard]] std::optional parse_args(int argc, char * argv[]); 55 | 56 | int get_screen_cols(); 57 | 58 | #endif // ARGS_HPP 59 | -------------------------------------------------------------------------------- /codecs/ani.cpp: -------------------------------------------------------------------------------- 1 | #include "ani.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include "binio.hpp" 12 | #include "sub_args.hpp" 13 | #include "ico.hpp" 14 | 15 | void Ani::open(std::istream & input, const Args & args) 16 | { 17 | constexpr auto id_size = 4u; 18 | constexpr auto anih_size = 36u; 19 | 20 | input.exceptions(std::ios_base::badbit | std::ios_base::failbit); 21 | try 22 | { 23 | input.ignore(id_size); // RIFF 24 | 25 | std::uint32_t file_size; 26 | readb(input, file_size); 27 | 28 | input.ignore(id_size); // ICON 29 | auto file_pos = id_size; 30 | 31 | std::vector frames; 32 | 33 | bool header_read = false; 34 | std::uint32_t num_frames {0}; 35 | std::uint32_t animation_steps {0}; 36 | std::uint32_t delay_count {0}; // in 1/60th of a second 37 | 38 | std::vector rate; 39 | std::vector seq; 40 | 41 | while(file_pos < file_size) 42 | { 43 | auto chunk_tag = readstr(input, id_size); 44 | file_pos += id_size; 45 | if(chunk_tag == "fram") 46 | continue; 47 | 48 | std::uint32_t chunk_size; 49 | readb(input, chunk_size); 50 | file_pos += sizeof(chunk_size); 51 | 52 | if(chunk_tag == "LIST") 53 | { 54 | // do nothing 55 | } 56 | else if(chunk_tag == "INAM" || chunk_tag == "IART") 57 | { 58 | input.ignore(chunk_size); 59 | file_pos += chunk_size; 60 | } 61 | else if(chunk_tag == "anih") 62 | { 63 | if(chunk_size != anih_size) 64 | throw std::runtime_error{"Error reading ANI: Invalid size for ani header"}; 65 | 66 | if(header_read) 67 | throw std::runtime_error{"Error reading ANI: multiple ani headers detected"}; 68 | 69 | input.ignore(sizeof(std::uint32_t)); // bytes in header 70 | readb(input, num_frames); 71 | readb(input, animation_steps); 72 | input.ignore(sizeof(std::uint32_t) * 4); // reserved 73 | readb(input, delay_count); 74 | input.ignore(sizeof(std::uint32_t)); // flags 75 | 76 | file_pos += anih_size; 77 | header_read = true; 78 | } 79 | else if(chunk_tag == "rate") 80 | { 81 | for(auto i = 0u; i < chunk_size / sizeof(std::uint32_t); ++i) 82 | { 83 | rate.emplace_back(); 84 | readb(input, rate.back()); 85 | } 86 | file_pos += chunk_size; 87 | } 88 | else if(chunk_tag == "seq ") 89 | { 90 | for(auto i = 0u; i < chunk_size / sizeof(std::uint32_t); ++i) 91 | { 92 | seq.emplace_back(); 93 | readb(input, seq.back()); 94 | } 95 | file_pos += chunk_size; 96 | } 97 | else if(chunk_tag == "icon") 98 | { 99 | auto ico = std::string(chunk_size, '\0'); 100 | input.read(std::data(ico), chunk_size); 101 | auto ico_input = std::istringstream{ico}; 102 | 103 | frames.emplace_back(); 104 | frames.back().open(ico_input, args); 105 | file_pos += chunk_size; 106 | } 107 | else 108 | { 109 | throw std::runtime_error{"Error reading ANI: Unrecognized chunk: " + chunk_tag}; 110 | } 111 | } 112 | 113 | if(std::size(frames) != num_frames) 114 | throw std::runtime_error{"Error reading ANI: frame count mismatched"}; 115 | 116 | if(!std::empty(rate) && std::size(rate) != animation_steps) 117 | throw std::runtime_error{"Error reading ANI: rate count mismatched"}; 118 | 119 | if(!std::empty(seq) && std::size(seq) != animation_steps) 120 | throw std::runtime_error{"Error reading ANI: seq count mismatched"}; 121 | 122 | if(animation_steps < num_frames) 123 | throw std::runtime_error{"Error reading ANI: not enough frames"}; 124 | 125 | if(num_frames == 0) 126 | throw std::runtime_error{"Error reading ANI: no frames"}; 127 | 128 | if(std::empty(seq)) 129 | { 130 | seq.resize(animation_steps); 131 | for(auto i = 0u, j = 0u; i < animation_steps; ++i) 132 | { 133 | seq[i] = j++; 134 | if(j == num_frames) 135 | j = 0; 136 | } 137 | } 138 | 139 | if(std::empty(rate)) 140 | rate = std::vector(animation_steps, delay_count); 141 | 142 | frame_delays_.reserve(animation_steps); 143 | images_.reserve(animation_steps); 144 | 145 | for(auto i = 0u; i < animation_steps; ++i) 146 | { 147 | frame_delays_.emplace_back(rate[i] / 60.0f); 148 | images_.emplace_back(frames.at(seq.at(i))); 149 | } 150 | 151 | move_image_data(images_.front()); 152 | images_.erase(std::begin(images_)); 153 | } 154 | catch(std::ios_base::failure & e) 155 | { 156 | if(input.bad()) 157 | throw std::runtime_error{"Error reading ANI: could not read file"}; 158 | else 159 | throw std::runtime_error{"Error reading ANI: unexpected end of file"}; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /codecs/ani.hpp: -------------------------------------------------------------------------------- 1 | #ifndef AVI_HPP 2 | #define AVI_HPP 3 | 4 | #include "image.hpp" 5 | 6 | inline bool is_ani(const Image::Header & header) 7 | { 8 | const std::array riff_header = {'R', 'I', 'F', 'F'}; 9 | const std::array acon_header = {'A', 'C', 'O', 'N'}; 10 | 11 | return std::equal(std::begin(riff_header), std::end(riff_header), std::begin(header), Image::header_cmp) && 12 | std::equal(std::begin(acon_header), std::end(acon_header), std::begin(header) + 8, Image::header_cmp); 13 | } 14 | 15 | class Ani final: public Image 16 | { 17 | public: 18 | Ani() = default; 19 | void open(std::istream & input, const Args & args) override; 20 | 21 | bool supports_multiple_images() const override { return true; } 22 | bool supports_animation() const override { return true; } 23 | }; 24 | 25 | #endif // AVI_HPP 26 | -------------------------------------------------------------------------------- /codecs/avif.cpp: -------------------------------------------------------------------------------- 1 | #include "avif.hpp" 2 | 3 | #include "avif/avif.h" 4 | 5 | #ifdef EXIF_FOUND 6 | #include "exif.hpp" 7 | #endif 8 | 9 | void Avif::open(std::istream & input, const Args &) 10 | { 11 | auto data = Image::read_input_to_memory(input); 12 | 13 | avifDecoder * decoder = avifDecoderCreate(); 14 | if(!decoder) 15 | throw std::runtime_error{"Error creating AVIF decoder"}; 16 | 17 | try 18 | { 19 | if(auto result = avifDecoderSetIOMemory(decoder, std::data(data), std::size(data)); result != AVIF_RESULT_OK) 20 | throw std::runtime_error{"Error setting AVIF IO: " + std::string{avifResultToString(result)}}; 21 | 22 | if(auto result = avifDecoderParse(decoder); result != AVIF_RESULT_OK) 23 | throw std::runtime_error{"Error reading AVIF file: " + std::string{avifResultToString(result)}}; 24 | 25 | if(decoder->imageCount < 1) 26 | throw std::runtime_error{"Error reading AVIF: no image found"}; 27 | 28 | if(auto result = avifDecoderNextImage(decoder); result != AVIF_RESULT_OK) 29 | throw std::runtime_error{"Error reading AVIF 1st frame: " + std::string{avifResultToString(result)}}; 30 | 31 | auto orientation { exif::Orientation::r_0}; 32 | #ifdef EXIF_FOUND 33 | if(decoder->image->exif.size > 0) 34 | orientation = exif::get_orientation(decoder->image->exif.data, decoder->image->exif.size).value_or(orientation); 35 | #endif // EXIF_FOUND 36 | 37 | set_size(decoder->image->width, decoder->image->height); 38 | 39 | avifRGBImage rgb; 40 | avifRGBImageSetDefaults(&rgb, decoder->image); 41 | rgb.format = AVIF_RGB_FORMAT_RGBA; 42 | rgb.depth = 8; 43 | 44 | avifRGBImageAllocatePixels(&rgb); 45 | avifImageYUVToRGB(decoder->image, &rgb); 46 | 47 | for(std::size_t row = 0; row < height_; ++row) 48 | { 49 | for(std::size_t col = 0; col < width_; ++col) 50 | { 51 | auto pixel = rgb.pixels + row * rgb.rowBytes + col * 4; 52 | image_data_[row][col] = Color{ pixel[0], pixel[1], pixel[2], pixel[3] }; 53 | } 54 | } 55 | 56 | avifRGBImageFreePixels(&rgb); 57 | 58 | // rotate as needed 59 | transpose_image(orientation); 60 | } 61 | catch(...) 62 | { 63 | avifDecoderDestroy(decoder); 64 | throw; 65 | } 66 | 67 | avifDecoderDestroy(decoder); 68 | } 69 | 70 | void Avif::write(std::ostream & out, const Image & img, bool invert) 71 | { 72 | auto image = avifImageCreate(img.get_width(), img.get_height(), 8, AVIF_PIXEL_FORMAT_YUV420); 73 | 74 | image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; 75 | image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; 76 | image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT709; 77 | image->yuvRange = AVIF_RANGE_FULL; 78 | 79 | avifImageAllocatePlanes(image, AVIF_PLANES_YUV); 80 | 81 | avifRGBImage rgb; 82 | avifRGBImageSetDefaults(&rgb, image); 83 | 84 | rgb.depth = 8; 85 | rgb.format = AVIF_RGB_FORMAT_RGBA; 86 | 87 | avifRGBImageAllocatePixels(&rgb); 88 | 89 | rgb.rowBytes = img.get_width() * 4; 90 | 91 | for(std::size_t row = 0; row < img.get_height(); ++row) 92 | { 93 | for(std::size_t col = 0; col < img.get_height(); ++col) 94 | { 95 | auto & color = img[row][col]; 96 | 97 | auto pixel = rgb.pixels + row * rgb.rowBytes + col * 4; 98 | for(std::size_t i = 0; i < 4; ++i) 99 | { 100 | if(invert && i < 4) 101 | pixel[i] = 255 - color[i]; 102 | else 103 | pixel[i] = color[i]; 104 | } 105 | } 106 | } 107 | 108 | avifImageRGBToYUV(image, &rgb); 109 | avifRGBImageFreePixels(&rgb); 110 | 111 | avifRWData output = AVIF_DATA_EMPTY; 112 | auto encoder = avifEncoderCreate(); 113 | encoder->maxThreads = 1; 114 | encoder->minQuantizer = AVIF_QUANTIZER_LOSSLESS; 115 | encoder->maxQuantizer = AVIF_QUANTIZER_LOSSLESS; 116 | 117 | if(auto result = avifEncoderWrite(encoder, image, &output); result != AVIF_RESULT_OK) 118 | { 119 | avifImageDestroy(image); 120 | avifRWDataFree(&output); 121 | avifEncoderDestroy(encoder); 122 | 123 | throw std::runtime_error{"Error writing AVIF file: " + std::string{avifResultToString(result)}}; 124 | } 125 | 126 | out.write(reinterpret_cast(output.data), output.size); 127 | 128 | avifImageDestroy(image); 129 | avifRWDataFree(&output); 130 | avifEncoderDestroy(encoder); 131 | } 132 | -------------------------------------------------------------------------------- /codecs/avif.hpp: -------------------------------------------------------------------------------- 1 | #ifndef AVIF_HPP 2 | #define AVIF_HPP 3 | 4 | // TODO: libheif is supposed to support reading AVIF files. I have not yet found 5 | // any AVIF files that it does work with, though. If libheif support improves in 6 | // the future, we can retire this module and use the heif module instead 7 | #include "image.hpp" 8 | 9 | #include 10 | 11 | inline bool is_avif(const Image::Header & header) 12 | { 13 | const std::array ftyp_header = {'f', 't', 'y', 'p'}; 14 | const std::array, 2> brand = {{ 15 | {'a', 'v', 'i', 'f'}, 16 | {'a', 'v', 'i', 's'}, 17 | }}; 18 | 19 | if(!std::equal(std::begin(ftyp_header), std::end(ftyp_header), std::begin(header) + 4, Image::header_cmp)) 20 | return false; 21 | 22 | return std::any_of(std::begin(brand), std::end(brand), [&header](const auto & b) 23 | { 24 | return std::equal(std::begin(b), std::end(b), std::begin(header) + 8, Image::header_cmp); 25 | }); 26 | } 27 | 28 | #ifdef AVIF_FOUND 29 | class Avif final: public Image 30 | { 31 | public: 32 | Avif() = default; 33 | void open(std::istream & input, const Args & args) override; 34 | 35 | static void write(std::ostream & out, const Image & img, bool invert); 36 | }; 37 | #endif 38 | #endif // AVIF_HPP 39 | -------------------------------------------------------------------------------- /codecs/binio.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BINIO_HPP 2 | #define BINIO_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | template concept Byte_input_iter = 13 | #ifdef _cpp_lib_concepts 14 | std::input_iterator && 15 | #else 16 | // poor man's input_iterator 17 | requires(T begin, T end) 18 | { 19 | begin == end; 20 | ++begin; 21 | *begin; 22 | *begin++; 23 | requires std::is_same_v; 24 | requires std::is_same_v; 25 | } && 26 | #endif 27 | requires { requires sizeof(*std::declval()) == 1; }; 28 | 29 | template concept Byte_output_iter = 30 | #ifdef _cpp_lib_concepts 31 | std::output_iterator; 32 | #else 33 | // poor man's output_iterator 34 | requires(T begin) 35 | { 36 | *begin; 37 | ++begin; 38 | begin++; 39 | *begin++; 40 | requires std::is_same_v; 41 | }; 42 | #endif 43 | 44 | // mixed endian systems apparently do exist, so do a static_assert to make sure we're one or the other 45 | static_assert(std::endian::native == std::endian::big || std::endian::native == std::endian::little); 46 | 47 | template 48 | T bswap(T a) 49 | { 50 | auto & buf = reinterpret_cast(a); 51 | 52 | if constexpr(sizeof(T) == 1) 53 | return a; 54 | else if constexpr(sizeof(T) == 2) 55 | { 56 | #if defined(__GNUC__) // also clang 57 | *reinterpret_cast(buf) = __builtin_bswap16(*reinterpret_cast(buf)); 58 | #elif defined(_MSC_VER) 59 | *reinterpret_cast(buf) = _byteswap_ushort(*reinterpret_cast(&buf)); 60 | #else 61 | std::swap(buf[0], buf[1]); 62 | #endif 63 | } 64 | else if constexpr(sizeof(T) == 4) 65 | { 66 | #if defined(__GNUC__) // also clang 67 | *reinterpret_cast(buf) = __builtin_bswap32(*reinterpret_cast(&buf)); 68 | #elif defined(_MSC_VER) 69 | *reinterpret_cast(buf) = _byteswap_ulong(*reinterpret_cast(&buf)); 70 | #else 71 | std::swap(buf[0], buf[3]); 72 | std::swap(buf[1], buf[2]); 73 | #endif 74 | } 75 | else if constexpr(sizeof(T) == 8) 76 | { 77 | #if defined(__GNUC__) // also clang 78 | *reinterpret_cast(buf) = __builtin_bswap64(*reinterpret_cast(&buf)); 79 | #elif defined(_MSC_VER) 80 | *reinterpret_cast(buf) = _byteswap_uint64(*reinterpret_cast(&buf)); 81 | #else 82 | std::swap(buf[0], buf[7]); 83 | std::swap(buf[1], buf[6]); 84 | std::swap(buf[2], buf[5]); 85 | std::swap(buf[3], buf[4]); 86 | #endif 87 | } 88 | else 89 | static_assert(sizeof(T) != 1 && sizeof(T) != 2 && sizeof(T) != 4 && sizeof(T) != 8, "unsupported type for bswap"); 90 | 91 | return a; 92 | } 93 | 94 | template requires (!std::is_enum_v) 95 | void readb(std::istream & i, T & t, std::endian endian = std::endian::little) 96 | { 97 | auto & buf = reinterpret_cast(t); 98 | i.read(buf, sizeof(buf)); 99 | if(std::endian::native != endian) 100 | t = bswap(t); 101 | } 102 | 103 | template requires (!std::is_enum_v) 104 | T readb(std::istream & i, std::endian endian = std::endian::little) 105 | { 106 | T t{0}; 107 | readb(i, t, endian); 108 | return t; 109 | } 110 | 111 | template requires (!std::is_enum_v) 112 | void readb(InputIter & begin, InputIter end, T & t, std::endian endian = std::endian::little) 113 | { 114 | auto & buf = reinterpret_cast(t); 115 | for(auto && i: buf) 116 | { 117 | if(begin == end) 118 | throw std::runtime_error{"Unexpected end of input"}; 119 | i = static_cast(*begin++); 120 | } 121 | if(std::endian::native != endian) 122 | t = bswap(t); 123 | } 124 | 125 | template requires (!std::is_enum_v) 126 | T readb(InputIter & begin, InputIter end, std::endian endian = std::endian::little) 127 | { 128 | T t{0}; 129 | readb(begin, end, t, endian); 130 | return t; 131 | } 132 | 133 | template requires std::is_enum_v 134 | void readb(std::istream & i, E & e, std::endian endian = std::endian::little) 135 | { 136 | readb>(i, reinterpret_cast&>(e), endian); 137 | } 138 | 139 | template requires std::is_enum_v 140 | E readb(std::istream & i, std::endian endian = std::endian::little) 141 | { 142 | return static_cast(readb>(i, endian)); 143 | } 144 | 145 | template requires std::is_enum_v 146 | void readb(InputIter & begin, InputIter end, E & e, std::endian endian = std::endian::little) 147 | { 148 | readb>(begin, end, reinterpret_cast&>(e), endian); 149 | } 150 | 151 | template requires std::is_enum_v 152 | E readb(InputIter & begin, InputIter end, std::endian endian = std::endian::little) 153 | { 154 | return static_cast(readb>(begin, end, endian)); 155 | } 156 | 157 | inline std::string readstr(std::istream & i, std::size_t len) 158 | { 159 | std::string s(len, ' '); 160 | i.read(std::data(s), len); 161 | 162 | return s; 163 | } 164 | 165 | template 166 | std::string readstr(InputIter & begin, InputIter end, std::size_t len) 167 | { 168 | std::string s(len, '\0'); 169 | for(std::size_t i = 0; i < len; ++i) 170 | { 171 | if(begin == end) 172 | throw std::runtime_error{"Unexpected end of input"}; 173 | s[i] = static_cast(*begin++); 174 | } 175 | return s; 176 | } 177 | 178 | template requires(!std::is_enum_v) 179 | void writeb(std::ostream & o, T t, std::endian endian = std::endian::little) 180 | { 181 | if(std::endian::native != endian) 182 | t = bswap(t); 183 | auto & buf = reinterpret_cast(t); 184 | o.write(buf, sizeof(buf)); 185 | } 186 | 187 | template requires(!std::is_enum_v) 188 | void writeb(OutputIter & o, T t, std::endian endian = std::endian::little) 189 | { 190 | if(std::endian::native != endian) 191 | t = bswap(t); 192 | auto & buf = reinterpret_cast(t); 193 | for(auto && b: buf) 194 | *o++ = b; 195 | } 196 | 197 | template requires(std::is_enum_v) 198 | void writeb(std::ostream & o, E e, std::endian endian = std::endian::little) 199 | { 200 | writeb(o, static_cast>(e), endian); 201 | } 202 | 203 | template requires(std::is_enum_v) 204 | void writeb(OutputIter & o, E e, std::endian endian = std::endian::little) 205 | { 206 | writeb(o, static_cast>(e), endian); 207 | } 208 | 209 | inline void writestr(std::ostream & o, const std::string_view & s) 210 | { 211 | o.write(std::data(s), std::size(s)); 212 | } 213 | 214 | template 215 | inline void writestr(OutputIter & o, const std::string_view & s) 216 | { 217 | for(auto && b: s) 218 | *o++ = b; 219 | } 220 | 221 | #endif // BINIO_HPP 222 | -------------------------------------------------------------------------------- /codecs/bitstream.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BITSTREAM_HPP 2 | #define BITSTREAM_HPP 3 | 4 | #include "binio.hpp" 5 | 6 | template 7 | class Input_bitstream 8 | { 9 | public: 10 | explicit Input_bitstream(InputIter in): 11 | in_{in} 12 | {} 13 | 14 | template 15 | T read(std::uint8_t bits) 16 | { 17 | T ret{0}; 18 | for(std::uint8_t i = 0u; i < bits; ++i) 19 | { 20 | if(bits_available_ == 0u) 21 | { 22 | bits_available_ = 8u; 23 | read_ = *in_++; 24 | } 25 | ret <<= 1u; 26 | ret |= (read_ >> --bits_available_) & 0x01u; 27 | } 28 | return ret; 29 | } 30 | 31 | std::uint8_t operator()(std::uint8_t bits) 32 | { 33 | return read(bits); 34 | } 35 | 36 | private: 37 | InputIter in_; 38 | std::uint8_t read_{0u}; 39 | std::uint8_t bits_available_{0u}; 40 | }; 41 | 42 | template 43 | class Output_bitstream 44 | { 45 | public: 46 | explicit Output_bitstream(OutputIter out): 47 | out_{out} 48 | {} 49 | 50 | ~Output_bitstream() 51 | { 52 | flush_current_byte(); 53 | } 54 | 55 | Output_bitstream(const Output_bitstream &) = default; 56 | Output_bitstream & operator=(const Output_bitstream &) = default; 57 | Output_bitstream(Output_bitstream &&) = default; 58 | Output_bitstream & operator=(Output_bitstream &&) = default; 59 | 60 | template 61 | void write(const T & t, std::uint8_t bits) 62 | { 63 | for(auto i = bits; i-- > 0u;) 64 | { 65 | if(bits_available_ == 0u) 66 | { 67 | bits_available_ = 8u; 68 | *out_++ = written_; 69 | written_ = 0u; 70 | } 71 | written_ <<= 1; 72 | written_ |= (t >> i) & 0x01u; 73 | --bits_available_; 74 | } 75 | } 76 | 77 | template 78 | void operator()(const T & t, std::uint8_t bits) 79 | { 80 | write(t, bits); 81 | } 82 | 83 | void flush_current_byte() 84 | { 85 | if(bits_available_ < 8u) 86 | { 87 | written_ <<= bits_available_; 88 | *out_++ = written_; 89 | bits_available_ = 8u; 90 | } 91 | } 92 | 93 | private: 94 | OutputIter out_; 95 | std::uint8_t written_{0u}; 96 | std::uint8_t bits_available_{8u}; 97 | }; 98 | #endif // BITSTREAM_HPP 99 | -------------------------------------------------------------------------------- /codecs/bmp.cpp: -------------------------------------------------------------------------------- 1 | #include "bmp.hpp" 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "bmp_common.hpp" 8 | 9 | void Bmp::open(std::istream & input, const Args &) 10 | { 11 | input.exceptions(std::ios_base::badbit | std::ios_base::failbit); 12 | try 13 | { 14 | std::size_t file_pos {0}; // need to keep track of where we are in the file because in.tellg() fails on at least some pipe inputs 15 | 16 | bmp_data bmp; 17 | read_bmp_file_header(input, bmp, file_pos); 18 | read_bmp_info_header(input, bmp, file_pos); 19 | 20 | set_size(bmp.width, bmp.height); 21 | 22 | read_bmp_data(input, bmp, file_pos, image_data_); 23 | } 24 | catch(std::ios_base::failure&) 25 | { 26 | if(input.bad()) 27 | throw std::runtime_error{"Error reading BMP: could not read file"}; 28 | else 29 | throw std::runtime_error{"Error reading BMP: unexpected end of file"}; 30 | } 31 | } 32 | 33 | void Bmp::write(std::ostream & out, const Image & img, bool invert) 34 | { 35 | write_bmp_file_header(out, img.get_width(), img.get_height()); 36 | write_bmp_info_header(out, img.get_width(), img.get_height()); 37 | write_bmp_data(out, img, invert); 38 | } 39 | -------------------------------------------------------------------------------- /codecs/bmp.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BMP_HPP 2 | #define BMP_HPP 3 | 4 | #include "image.hpp" 5 | 6 | inline bool is_bmp(const Image::Header & header) 7 | { 8 | const std::array bmp_header {0x42, 0x4D}; 9 | return std::equal(std::begin(bmp_header), std::end(bmp_header), std::begin(header), Image::header_cmp); 10 | } 11 | 12 | class Bmp final: public Image 13 | { 14 | public: 15 | Bmp() = default; 16 | void open(std::istream & input, const Args & args) override; 17 | 18 | static void write(std::ostream & out, const Image & img, bool invert); 19 | }; 20 | #endif // BMP_HPP 21 | -------------------------------------------------------------------------------- /codecs/bmp_common.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BMP_COMMON_HPP 2 | #define BMP_COMMON_HPP 3 | 4 | #include 5 | #include 6 | 7 | #include "image.hpp" 8 | 9 | struct bmp_data 10 | { 11 | std::uint32_t pixel_offset {0}; 12 | std::size_t width{0}; 13 | std::size_t height{0}; 14 | bool bottom_to_top {true}; 15 | std::uint16_t bpp {0}; 16 | enum class Compression: std::uint32_t {BI_RGB=0, BI_RLE8=1, BI_RLE4=2, BI_BITFIELDS=3} compression{Compression::BI_RGB}; 17 | std::uint32_t palette_size {0}; 18 | std::uint32_t red_mask {0}; 19 | std::uint32_t green_mask {0}; 20 | std::uint32_t blue_mask {0}; 21 | std::uint32_t alpha_mask {0}; 22 | 23 | std::vector palette; 24 | }; 25 | 26 | void read_bmp_file_header(std::istream & in, bmp_data & bmp, std::size_t & file_pos); 27 | void read_bmp_info_header(std::istream & in, bmp_data & bmp, std::size_t & file_pos); 28 | void read_bmp_data(std::istream & in, const bmp_data & bmp, std::size_t & file_pos, std::vector> & image_data); 29 | 30 | // writing functions create a V4 or V1 32bpp RGBA bitmap 31 | void write_bmp_file_header(std::ostream & out, std::uint32_t width, std::uint32_t height, bool v4_header = true); 32 | void write_bmp_info_header(std::ostream & out, std::uint32_t width, std::uint32_t height, bool v4_header = true, bool double_height_for_ico_and_mask = false); 33 | void write_bmp_data(std::ostream & out, const Image & img, bool invert); 34 | #endif // BMP_COMMON_HPP 35 | -------------------------------------------------------------------------------- /codecs/bpg.cpp: -------------------------------------------------------------------------------- 1 | #include "bpg.hpp" 2 | 3 | #include 4 | 5 | #ifdef EXIF_FOUND 6 | #include "exif.hpp" 7 | #endif 8 | 9 | void Bpg::open(std::istream & input, const Args &) 10 | { 11 | auto data = Image::read_input_to_memory(input); 12 | 13 | auto decoder = bpg_decoder_open(); 14 | if(!decoder) 15 | throw std::runtime_error{"Could not create BPG decoder"}; 16 | 17 | bpg_decoder_keep_extension_data(decoder, true); 18 | 19 | if(bpg_decoder_decode(decoder, std::data(data), std::size(data)) != 0) 20 | { 21 | bpg_decoder_close(decoder); 22 | throw std::runtime_error{"Could not decode BPG image"}; 23 | } 24 | 25 | auto orientation { exif::Orientation::r_0}; 26 | #ifdef EXIF_FOUND 27 | auto ext = bpg_decoder_get_extension_data(decoder); 28 | while(ext) 29 | { 30 | if(ext->tag == BPG_EXTENSION_TAG_EXIF) 31 | { 32 | std::vector exif_buf(ext->buf_len + 6); 33 | for(auto i = 0; i < 6; ++i) 34 | exif_buf[i] = "Exif\0\0"[i]; 35 | std::copy(ext->buf, ext->buf + ext->buf_len, std::begin(exif_buf) + 6); 36 | orientation = exif::get_orientation(std::data(exif_buf), std::size(exif_buf)).value_or(orientation); 37 | } 38 | ext = ext->next; 39 | } 40 | #endif 41 | 42 | BPGImageInfo info; 43 | if(bpg_decoder_get_info(decoder, &info) != 0) 44 | { 45 | bpg_decoder_close(decoder); 46 | throw std::runtime_error{"Could not get BPG info"}; 47 | } 48 | 49 | set_size(info.width, info.height); 50 | 51 | if(bpg_decoder_start(decoder, BPG_OUTPUT_FORMAT_RGBA32) != 0) 52 | { 53 | bpg_decoder_close(decoder); 54 | throw std::runtime_error{"Could not convert BPG image"}; 55 | } 56 | 57 | std::vector row_buffer(width_ * 4); 58 | for(std::size_t row = 0; row < height_; ++row) 59 | { 60 | if(bpg_decoder_get_line(decoder, std::data(row_buffer)) != 0) 61 | { 62 | bpg_decoder_close(decoder); 63 | throw std::runtime_error{"Could not read BPG image"}; 64 | } 65 | for(std::size_t col = 0; col < width_; ++col) 66 | image_data_[row][col] = { row_buffer[col * 4], 67 | row_buffer[col * 4 + 1], 68 | row_buffer[col * 4 + 2], 69 | row_buffer[col * 4 + 3] }; 70 | } 71 | 72 | bpg_decoder_close(decoder); 73 | 74 | transpose_image(orientation); 75 | } 76 | -------------------------------------------------------------------------------- /codecs/bpg.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BPG_HPP 2 | #define BPG_HPP 3 | 4 | #include "image.hpp" 5 | 6 | #include 7 | 8 | inline bool is_bpg(const Image::Header & header) 9 | { 10 | const std::array bpg_header = {'B', 'P', 'G', 0xFB}; 11 | return std::equal(std::begin(bpg_header), std::end(bpg_header), std::begin(header), Image::header_cmp); 12 | } 13 | 14 | #ifdef BPG_FOUND 15 | class Bpg final: public Image 16 | { 17 | public: 18 | Bpg() = default; 19 | void open(std::istream & input, const Args & args) override; 20 | }; 21 | #endif 22 | #endif // BPG_HPP 23 | -------------------------------------------------------------------------------- /codecs/exif.cpp: -------------------------------------------------------------------------------- 1 | #include "exif.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | namespace exif 10 | { 11 | std::optional get_orientation(const unsigned char * data , std::size_t len) 12 | { 13 | auto orientation = std::optional{}; 14 | auto exif_data = exif_data_new_from_data(data, len); 15 | if(exif_data) 16 | { 17 | auto orientation_entry = exif_data_get_entry(exif_data, EXIF_TAG_ORIENTATION); 18 | if(orientation_entry && orientation_entry->format == EXIF_FORMAT_SHORT) 19 | { 20 | orientation = static_cast(exif_get_short(orientation_entry->data, exif_data_get_byte_order(exif_data))); 21 | if(orientation != Orientation::r_0 && orientation != Orientation::r_180 && orientation != Orientation::r_270 && orientation != Orientation::r_90) 22 | { 23 | exif_data_unref(exif_data); 24 | std::vector desc(256); 25 | exif_entry_get_value(orientation_entry, std::data(desc), std::size(desc)); 26 | throw std::runtime_error{"Unsupported EXIF rotation: " + std::string{std::data(desc)} + " (" + std::to_string(static_cast>(*orientation)) + ")"}; 27 | } 28 | } 29 | exif_data_unref(exif_data); 30 | } 31 | return orientation; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /codecs/exif.hpp: -------------------------------------------------------------------------------- 1 | #ifndef EXIF_HPP 2 | #define EXIF_HPP 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "config.h" 9 | 10 | namespace exif 11 | { 12 | enum class Orientation:short { r_0=1, r_180=3, r_270=6, r_90=8 }; 13 | 14 | #ifdef EXIF_FOUND 15 | std::optional get_orientation(const unsigned char * data, std::size_t len); 16 | #endif // EXIF_FOUND 17 | } 18 | 19 | #endif // EXIF_HPP 20 | -------------------------------------------------------------------------------- /codecs/flif.cpp: -------------------------------------------------------------------------------- 1 | #include "flif.hpp" 2 | 3 | #ifdef FLIF_ENC_FOUND 4 | #include 5 | #elif FLIF_DEC_FOUND 6 | #include 7 | #endif 8 | 9 | #ifdef EXIF_FOUND 10 | #include "exif.hpp" 11 | #endif 12 | 13 | #ifdef FLIF_DEC_FOUND 14 | void Flif::open(std::istream & input, const Args &) 15 | { 16 | auto data = Image::read_input_to_memory(input); 17 | 18 | auto decoder = flif_create_decoder(); 19 | if(!decoder) 20 | throw std::runtime_error{"Could not create FLIF decoder"}; 21 | 22 | if(!flif_decoder_decode_memory(decoder, std::data(data), std::size(data))) 23 | { 24 | flif_abort_decoder(decoder); 25 | throw std::runtime_error{"Could not decode FLIF image"}; 26 | } 27 | 28 | auto image = flif_decoder_get_image(decoder, 0); 29 | if(!image) 30 | { 31 | flif_destroy_decoder(decoder); 32 | throw std::runtime_error{"Could not get FLIF image"}; 33 | } 34 | 35 | auto orientation { exif::Orientation::r_0}; 36 | #ifdef EXIF_FOUND 37 | unsigned char * metadata = nullptr; 38 | std::size_t metadata_size = 0; 39 | if(flif_image_get_metadata(image, "eXif", &metadata, &metadata_size)) 40 | { 41 | orientation = exif::get_orientation(metadata, metadata_size).value_or(orientation); 42 | flif_image_free_metadata(image, metadata); 43 | } 44 | #endif 45 | 46 | set_size(flif_image_get_width(image), flif_image_get_height(image)); 47 | 48 | std::vector row_buffer(width_ * 4); 49 | for(std::size_t row = 0; row < height_; ++row) 50 | { 51 | flif_image_read_row_RGBA8(image, row, std::data(row_buffer), std::size(row_buffer)); 52 | for(std::size_t col = 0; col < width_; ++col) 53 | image_data_[row][col] = { row_buffer[col * 4], 54 | row_buffer[col * 4 + 1], 55 | row_buffer[col * 4 + 2], 56 | row_buffer[col * 4 + 3] }; 57 | } 58 | 59 | flif_destroy_decoder(decoder); 60 | 61 | transpose_image(orientation); 62 | } 63 | #endif 64 | 65 | #ifdef FLIF_ENC_FOUND 66 | void Flif::write(std::ostream & out, const Image & img, bool invert) 67 | { 68 | auto encoder = flif_create_encoder(); 69 | if(!encoder) 70 | throw std::runtime_error{"Could not create FLIF encoder"}; 71 | 72 | auto image = flif_create_image(img.get_width(), img.get_height()); 73 | if(!image) 74 | { 75 | flif_destroy_encoder(encoder); 76 | throw std::runtime_error{"Could not create FLIF image"}; 77 | } 78 | 79 | std::vector row_buffer(img.get_width() *4); 80 | for(std::size_t row = 0; row < img.get_height(); ++row) 81 | { 82 | for(std::size_t col = 0; col < img.get_width(); ++col) 83 | { 84 | for(std::size_t i = 0; i < 4; ++i) 85 | { 86 | if(i < 3 && invert) 87 | row_buffer[col * 4 + i] = 255 - img[row][col][i]; 88 | else 89 | row_buffer[col * 4 + i] = img[row][col][i]; 90 | } 91 | } 92 | 93 | flif_image_write_row_RGBA8(image, row, std::data(row_buffer), std::size(row_buffer)); 94 | } 95 | 96 | flif_encoder_add_image_move(encoder, image); 97 | 98 | char * buffer; 99 | std::size_t buffer_size; 100 | 101 | flif_encoder_encode_memory(encoder, reinterpret_cast(&buffer), &buffer_size); 102 | flif_destroy_encoder(encoder); 103 | 104 | out.write(buffer, buffer_size); 105 | free(buffer); 106 | } 107 | #endif 108 | -------------------------------------------------------------------------------- /codecs/flif.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FLIF_HPP 2 | #define FLIF_HPP 3 | 4 | #include "image.hpp" 5 | 6 | #include 7 | 8 | inline bool is_flif(const Image::Header & header) 9 | { 10 | const std::array flif_header = {'F', 'L', 'I', 'F'}; 11 | return std::equal(std::begin(flif_header), std::end(flif_header), std::begin(header), Image::header_cmp); 12 | } 13 | 14 | #if defined(FLIF_ENC_FOUND) || defined(FLIF_DEC_FOUND) 15 | class Flif final: public Image 16 | { 17 | public: 18 | #ifdef FLIF_DEC_FOUND 19 | Flif() = default; 20 | void open(std::istream & input, const Args & args) override; 21 | #endif 22 | 23 | #ifdef FLIF_ENC_FOUND 24 | static void write(std::ostream & out, const Image & img, bool invert); 25 | #endif 26 | }; 27 | #endif 28 | #endif // FLIF_HPP 29 | -------------------------------------------------------------------------------- /codecs/gif.cpp: -------------------------------------------------------------------------------- 1 | #include "gif.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include "sub_args.hpp" 13 | 14 | // giflib is a very poorly designed library. Its documentation is even worse 15 | 16 | int read_fn(GifFileType* gif_file, GifByteType * data, int length) noexcept 17 | { 18 | auto in = static_cast(gif_file->UserData); 19 | if(!in) 20 | { 21 | std::cerr<<"FATAL ERROR: Could not get GIF input stream\n"; 22 | return GIF_ERROR; 23 | } 24 | 25 | in->read(reinterpret_cast(data), length); 26 | if(in->bad()) 27 | { 28 | std::cerr<<"FATAL ERROR: Could not read GIF image\n"; 29 | return GIF_ERROR; 30 | } 31 | 32 | return in->gcount(); 33 | } 34 | 35 | void Gif::open(std::istream & input, const Args &) 36 | { 37 | int error_code = GIF_OK; 38 | GifFileType * gif = DGifOpen(&input, read_fn, &error_code); 39 | if(!gif) 40 | throw std::runtime_error{"Error setting up GIF: " + std::string{GifErrorString(error_code)}}; 41 | 42 | if(DGifSlurp(gif) != GIF_OK) 43 | { 44 | DGifCloseFile(gif, NULL); 45 | throw std::runtime_error{"Error reading GIF: " + std::string{GifErrorString(gif->Error)}}; 46 | } 47 | 48 | set_size(gif->SWidth, gif->SHeight); 49 | 50 | // default all pixels to transparent 51 | for(std::size_t row = 0; row < height_; ++row) 52 | { 53 | for(std::size_t col = 0; col < width_; ++col) 54 | { 55 | image_data_[row][col] = Color{0, 0, 0, 0}; 56 | } 57 | } 58 | 59 | for(auto f = 0; f < gif->ImageCount; ++f) 60 | { 61 | auto pal = gif->SavedImages[f].ImageDesc.ColorMap; 62 | if(!pal) 63 | { 64 | pal = gif->SColorMap; 65 | if(!pal) 66 | { 67 | DGifCloseFile(gif, NULL); 68 | throw std::runtime_error{"Could not find color map"}; 69 | } 70 | } 71 | 72 | int transparency_ind = -1; 73 | 74 | GraphicsControlBlock gcb; 75 | if(DGifSavedExtensionToGCB(gif, f, &gcb) == GIF_OK) 76 | { 77 | transparency_ind = gcb.TransparentColor; 78 | frame_delays_.emplace_back(0.01f * gcb.DelayTime); 79 | } 80 | else 81 | frame_delays_.emplace_back(1.0f / 25.0f); 82 | 83 | auto left = gif->SavedImages[f].ImageDesc.Left; 84 | auto top = gif->SavedImages[f].ImageDesc.Top; 85 | auto sub_width = static_cast(gif->SavedImages[f].ImageDesc.Width); 86 | auto sub_height = static_cast(gif->SavedImages[f].ImageDesc.Height); 87 | 88 | if(left + sub_width > width_ || top + sub_height > height_) 89 | { 90 | DGifCloseFile(gif, NULL); 91 | throw std::runtime_error{"GIF has wrong size or offset"}; 92 | } 93 | 94 | const auto & im = gif->SavedImages[f].RasterBits; 95 | 96 | auto & frame = images_.emplace_back(width_, height_); 97 | for(std::size_t row = 0; row < sub_height; ++row) 98 | { 99 | for(std::size_t col = 0; col < sub_width; ++col) 100 | { 101 | auto index = im[row * sub_width + col]; 102 | 103 | if(index != transparency_ind) 104 | { 105 | auto & pal_color = pal->Colors[index]; 106 | auto c = Color{pal_color.Red, pal_color.Green, pal_color.Blue}; 107 | if(composed_) 108 | image_data_[row + top][col + left] = c; 109 | else 110 | frame[row + top][col + left] = c; 111 | } 112 | } 113 | } 114 | if(composed_) 115 | frame.copy_image_data(*this); 116 | } 117 | 118 | move_image_data(images_.front()); 119 | images_.erase(std::begin(images_)); 120 | DGifCloseFile(gif, NULL); 121 | } 122 | 123 | void Gif::handle_extra_args(const Args & args) 124 | { 125 | auto options = Sub_args{"GIF"}; 126 | try 127 | { 128 | options.add_options() 129 | ("not-composed", "Show only information for the given frame, not those leading up to it"); 130 | 131 | auto sub_args = options.parse(args.extra_args); 132 | 133 | composed_ = !sub_args.count("not-composed"); 134 | 135 | if(args.animate && !composed_) 136 | throw std::runtime_error{options.help(args.help_text) + "\nCan't specify --not-composed with --animate"}; 137 | } 138 | catch(const cxxopt_exception & e) 139 | { 140 | throw std::runtime_error{options.help(args.help_text) + '\n' + e.what()}; 141 | } 142 | } 143 | 144 | int write_fn(GifFileType * gif_file, const GifByteType * data, int length) 145 | { 146 | auto out = static_cast(gif_file->UserData); 147 | if(!out) 148 | { 149 | std::cerr<<"FATAL ERROR: Could not get GIF output stream\n"; 150 | return GIF_ERROR; 151 | } 152 | 153 | out->write(reinterpret_cast(data), length); 154 | if(out->bad()) 155 | { 156 | std::cerr<<"FATAL ERROR: Could not write GIF image\n"; 157 | return GIF_ERROR; 158 | } 159 | 160 | return length; 161 | } 162 | void Gif::write(std::ostream & out, const Image & img, bool invert) 163 | { 164 | int error_code = GIF_OK; 165 | GifFileType * gif = EGifOpen(&out, write_fn, &error_code); 166 | if(!gif) 167 | throw std::runtime_error{"Error setting up GIF: " + std::string{GifErrorString(error_code)}}; 168 | 169 | Image img_copy{img.get_width(), img.get_height()}; 170 | 171 | for(std::size_t row = 0; row < img.get_height(); ++row) 172 | { 173 | for(std::size_t col = 0; col < img.get_width(); ++col) 174 | { 175 | img_copy[row][col] = img[row][col]; 176 | if(invert) 177 | img_copy[row][col].invert(); 178 | } 179 | } 180 | 181 | auto palette = img_copy.generate_and_apply_palette(256, true); 182 | 183 | std::map color_lookup; 184 | std::vector gif_palette(std::size(palette)); 185 | 186 | for(std::size_t i = 0; i < std::size(palette); ++i) 187 | { 188 | auto & c = palette[i]; 189 | 190 | color_lookup[c] = i; 191 | gif_palette[i] = GifColorType{ c.r, c.g, c.b };; 192 | } 193 | 194 | // Gif generation doesn't seem to work when the # of colors in the palette is < 256, so pad it to that size 195 | if(auto old_size = std::size(gif_palette); old_size < 256) 196 | { 197 | gif_palette.resize(256); 198 | std::fill(std::begin(gif_palette) + old_size, std::end(gif_palette), GifColorType{0, 0, 0}); 199 | } 200 | 201 | auto gif_color_map = GifMakeMapObject(std::size(gif_palette), std::data(gif_palette)); 202 | gif_palette.clear(); 203 | 204 | gif->SWidth = img_copy.get_width(); 205 | gif->SHeight = img_copy.get_height(); 206 | gif->SColorResolution = 8; 207 | gif->SBackGroundColor = 0; 208 | gif->SColorMap = gif_color_map; 209 | 210 | auto gif_img = GifMakeSavedImage(gif, nullptr); 211 | if(!gif_img) 212 | { 213 | EGifCloseFile(gif, nullptr); 214 | throw std::runtime_error{"Error allocating GIF Image structure"}; 215 | } 216 | 217 | gif_img->ImageDesc.Left = 0; 218 | gif_img->ImageDesc.Top = 0; 219 | gif_img->ImageDesc.Width = img_copy.get_width(); 220 | gif_img->ImageDesc.Height = img_copy.get_height(); 221 | gif_img->ImageDesc.Interlace = false; 222 | gif_img->ImageDesc.ColorMap = nullptr; 223 | gif_img->ExtensionBlockCount = 0; 224 | gif_img->ExtensionBlocks = nullptr; 225 | gif_img->RasterBits = reinterpret_cast(malloc(img_copy.get_width() * img_copy.get_height())); 226 | if(!gif_img->RasterBits) 227 | { 228 | EGifCloseFile(gif, nullptr); 229 | GifFreeMapObject(gif_color_map); 230 | throw std::runtime_error{"Error allocating GIF image data"}; 231 | } 232 | 233 | for(std::size_t row = 0; row < img_copy.get_height(); ++row) 234 | { 235 | for(std::size_t col = 0; col < img_copy.get_width(); ++col) 236 | { 237 | try 238 | { 239 | gif_img->RasterBits[row * img_copy.get_width() + col] = color_lookup.at(img_copy[row][col]); 240 | } 241 | catch(const std::out_of_range &) 242 | { 243 | GifFreeSavedImages(gif); 244 | EGifCloseFile(gif, nullptr); 245 | GifFreeMapObject(gif_color_map); 246 | throw std::runtime_error{"Error finding GIF color in palette"}; 247 | } 248 | } 249 | } 250 | 251 | // TODO: build hash table 252 | auto transparent_color = std::find_if(std::begin(palette), std::end(palette), [](const Color & c) { return c.a == 0; }); 253 | if(transparent_color != std::end(palette)) 254 | { 255 | GraphicsControlBlock gcb; 256 | memset(&gcb, 0, sizeof(gcb)); 257 | gcb.TransparentColor = std::distance(std::begin(palette), transparent_color); 258 | if(auto error_code = EGifGCBToSavedExtension(&gcb, gif, 0); error_code != GIF_OK) 259 | { 260 | GifFreeSavedImages(gif); 261 | EGifCloseFile(gif, nullptr); 262 | GifFreeMapObject(gif_color_map); 263 | throw std::runtime_error{"Error setting GIF transparency index: " + std::string{GifErrorString(error_code)}}; 264 | } 265 | } 266 | 267 | // Prevent a memory leak here - EGifCloseFile / EGifSpew don't free SavedImages 268 | // save a pointer to them and clean it up afterwards 269 | 270 | auto saved_images = gif->SavedImages; 271 | 272 | if(auto error_code = EGifSpew(gif); error_code != GIF_OK) 273 | { 274 | GifFreeSavedImages(gif); 275 | EGifCloseFile(gif, nullptr); 276 | GifFreeMapObject(gif_color_map); 277 | throw std::runtime_error{"Error writing GIF to file"}; 278 | } 279 | 280 | if(saved_images) 281 | { 282 | if(saved_images[0].ExtensionBlocks) 283 | { 284 | free(saved_images[0].ExtensionBlocks[0].Bytes); 285 | free(saved_images[0].ExtensionBlocks); 286 | } 287 | free(saved_images[0].RasterBits); 288 | free(saved_images); 289 | } 290 | 291 | GifFreeMapObject(gif_color_map); 292 | } 293 | -------------------------------------------------------------------------------- /codecs/gif.hpp: -------------------------------------------------------------------------------- 1 | #ifndef GIF_HPP 2 | #define GIF_HPP 3 | 4 | #include "image.hpp" 5 | 6 | inline bool is_gif(const Image::Header & header) 7 | { 8 | const std::array gif_header1 = {0x47, 0x49, 0x46, 0x38, 0x37, 0x61}; //GIF87a 9 | const std::array gif_header2 = {0x47, 0x49, 0x46, 0x38, 0x39, 0x61}; //GIF89a 10 | 11 | return std::equal(std::begin(gif_header1), std::end(gif_header1), std::begin(header), Image::header_cmp) 12 | || std::equal(std::begin(gif_header2), std::end(gif_header2), std::begin(header), Image::header_cmp); 13 | } 14 | 15 | #ifdef GIF_FOUND 16 | class Gif final: public Image 17 | { 18 | public: 19 | Gif() = default; 20 | void open(std::istream & input, const Args & args) override; 21 | 22 | void handle_extra_args(const Args & args) override; 23 | bool supports_multiple_images() const override { return true; } 24 | bool supports_animation() const override { return true; } 25 | 26 | static void write(std::ostream & out, const Image & img, bool invert); 27 | 28 | private: 29 | bool composed_ {true}; 30 | }; 31 | #endif 32 | #endif // GIF_HPP 33 | -------------------------------------------------------------------------------- /codecs/heif.cpp: -------------------------------------------------------------------------------- 1 | #include "heif.hpp" 2 | 3 | #include 4 | 5 | #ifdef EXIF_FOUND 6 | #include "exif.hpp" 7 | #endif 8 | 9 | void Heif::open(std::istream & input, const Args &) 10 | { 11 | auto data = Image::read_input_to_memory(input); 12 | 13 | heif::Context context; 14 | try 15 | { 16 | context.read_from_memory_without_copy(std::data(data), std::size(data)); 17 | auto handle = context.get_primary_image_handle(); 18 | 19 | auto orientation { exif::Orientation::r_0}; 20 | #ifdef EXIF_FOUND 21 | auto metadata_ids = handle.get_list_of_metadata_block_IDs("Exif"); 22 | for(auto && id: metadata_ids) 23 | { 24 | auto metadata = handle.get_metadata(id); 25 | // libheif documentation says to skip the first 4 bytes of exif metadata 26 | orientation = exif::get_orientation(std::data(metadata) + 4, std::size(metadata) - 4).value_or(orientation); 27 | } 28 | #endif // EXIF_FOUND 29 | 30 | auto image = handle.decode_image(heif_colorspace_RGB, heif_chroma_interleaved_RGBA); 31 | 32 | set_size(image.get_width(heif_channel_interleaved), image.get_height(heif_channel_interleaved)); 33 | 34 | int row_stride = 0; 35 | auto plane = image.get_plane(heif_channel_interleaved, &row_stride); 36 | if(!plane) 37 | throw std::runtime_error{"Error reading HEIF image data"}; 38 | 39 | for(std::size_t row = 0; row < height_; ++row) 40 | { 41 | for(std::size_t col = 0; col < width_; ++col) 42 | { 43 | if(image.get_chroma_format() == heif_chroma_interleaved_RGBA) 44 | { 45 | image_data_[row][col] = Color{plane[row * row_stride + col * 4], 46 | plane[row * row_stride + col * 4 + 1], 47 | plane[row * row_stride + col * 4 + 2], 48 | plane[row * row_stride + col * 4 + 3]}; 49 | } 50 | else if(image.get_chroma_format() == heif_chroma_interleaved_RGB) 51 | { 52 | image_data_[row][col] = Color{plane[row * row_stride + col * 3], 53 | plane[row * row_stride + col * 3 + 1], 54 | plane[row * row_stride + col * 3 + 2]}; 55 | } 56 | else if(image.get_chroma_format() == heif_chroma_monochrome) 57 | { 58 | image_data_[row][col] = Color{plane[row * row_stride + col]}; 59 | } 60 | } 61 | } 62 | 63 | // rotate as needed 64 | transpose_image(orientation); 65 | } 66 | catch(const heif::Error & e) 67 | { 68 | throw std::runtime_error{"Error reading HEIF file: " + e.get_message()}; 69 | } 70 | } 71 | 72 | void Heif::write(std::ostream & out, const Image & img, bool invert) 73 | { 74 | try 75 | { 76 | heif::Image image; 77 | image.create(img.get_width(), img.get_height(), heif_colorspace_RGB, heif_chroma_interleaved_RGBA); 78 | image.add_plane(heif_channel_interleaved, img.get_width(), img.get_height(), 8); 79 | 80 | int row_stride = 0; 81 | auto plane = image.get_plane(heif_channel_interleaved, &row_stride); 82 | if(!plane) 83 | throw std::runtime_error{"Error writing HEIF image data"}; 84 | 85 | for(std::size_t row = 0; row < img.get_height(); ++row) 86 | { 87 | for(std::size_t col = 0; col < img.get_width(); ++col) 88 | { 89 | for(std::size_t i = 0; i < 4; ++i) 90 | { 91 | if(invert && i < 3) 92 | plane[row * row_stride + col * 4 + i] = 255 - img[row][col][i]; 93 | else 94 | plane[row * row_stride + col * 4 + i] = img[row][col][i]; 95 | } 96 | } 97 | } 98 | 99 | heif::Context context; 100 | heif::Encoder encoder(heif_compression_HEVC); 101 | context.encode_image(image, encoder); 102 | 103 | class Ostream_writer: public heif::Context::Writer 104 | { 105 | public: 106 | explicit Ostream_writer(std::ostream & out): out_{out} {}; 107 | heif_error write(const void* data, size_t size) override 108 | { 109 | out_.write(static_cast(data), size); 110 | return out_ ? heif_error{heif_error_Ok, heif_suberror_Unspecified, ""} : heif_error{heif_error_Encoding_error, heif_suberror_Cannot_write_output_data, "Output error"}; 111 | } 112 | private: 113 | std::ostream & out_; 114 | } writer{out}; 115 | 116 | context.write(writer); 117 | } 118 | catch(const heif::Error & e) 119 | { 120 | throw std::runtime_error{"Error writing HEIF file: " + e.get_message()}; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /codecs/heif.hpp: -------------------------------------------------------------------------------- 1 | #ifndef HEIF_HPP 2 | #define HEIF_HPP 3 | 4 | #include "image.hpp" 5 | 6 | #include 7 | 8 | inline bool is_heif(const Image::Header & header) 9 | { 10 | const std::array ftyp_header = {'f', 't', 'y', 'p'}; 11 | const std::array, 10> brand = {{ 12 | {'h', 'e', 'i', 'c'}, 13 | {'h', 'e', 'i', 'x'}, 14 | {'h', 'e', 'v', 'c'}, 15 | {'h', 'e', 'v', 'x'}, 16 | {'h', 'e', 'i', 'm'}, 17 | {'h', 'e', 'i', 's'}, 18 | {'h', 'e', 'v', 'm'}, 19 | {'h', 'e', 'v', 's'}, 20 | {'m', 'i', 'f', '1'}, 21 | {'m', 's', 'f', '1'}, 22 | }}; 23 | 24 | if(!std::equal(std::begin(ftyp_header), std::end(ftyp_header), std::begin(header) + 4, Image::header_cmp)) 25 | return false; 26 | 27 | return std::any_of(std::begin(brand), std::end(brand), [&header](const auto & b) 28 | { 29 | return std::equal(std::begin(b), std::end(b), std::begin(header) + 8, Image::header_cmp); 30 | }); 31 | } 32 | 33 | #ifdef HEIF_FOUND 34 | class Heif final: public Image 35 | { 36 | public: 37 | Heif() = default; 38 | void open(std::istream & input, const Args & args) override; 39 | 40 | static void write(std::ostream & out, const Image & img, bool invert); 41 | }; 42 | #endif 43 | #endif // HEIF_HPP 44 | -------------------------------------------------------------------------------- /codecs/ico.cpp: -------------------------------------------------------------------------------- 1 | #include "ico.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "bmp_common.hpp" 9 | #include "png.hpp" 10 | #include "binio.hpp" 11 | 12 | struct Ico_header 13 | { 14 | std::uint8_t width {0}; 15 | std::uint8_t height {0}; 16 | std::uint32_t size {0}; 17 | std::uint32_t offset {0}; 18 | }; 19 | 20 | void Ico::open(std::istream & input, const Args & args) 21 | { 22 | input.exceptions(std::ios_base::badbit | std::ios_base::failbit); 23 | try 24 | { 25 | Ico_header ico; 26 | 27 | input.ignore(4); // skip 0 and type 28 | 29 | std::uint16_t num_images; 30 | readb(input, num_images); 31 | 32 | if(num_images == 0) 33 | throw std::runtime_error{"Error reading ICO / CUR: 0 images"}; 34 | 35 | readb(input, ico.width); 36 | readb(input, ico.height); 37 | input.ignore(6); // skip data we can get from the image itself 38 | readb(input, ico.size); 39 | readb(input, ico.offset); 40 | 41 | // skip to image data 42 | input.ignore(ico.offset - 6 - 16); // 6 for ICONDIR, 16 per ICONDIRENTRY 43 | 44 | // determine if image data is BMP or PNG 45 | Image::Header header; 46 | input.read(std::data(header), std::size(header)); 47 | for(auto i = std::rbegin(header); i != std::rend(header); ++i) 48 | input.putback(*i); 49 | 50 | if(is_png(header)) 51 | { 52 | #ifdef PNG_FOUND 53 | Png png_img; 54 | input.exceptions(std::ios_base::goodbit); // disable our own IO exceptions - PNG handles it differently 55 | png_img.open(input, args); 56 | move_image_data(png_img); 57 | #else 58 | throw std::runtime_error{"Could not read PNG encoded ICO / CUR: Not compiled with PNG support"}; 59 | #endif 60 | return; 61 | } 62 | 63 | // assuming BMP format 64 | width_ = ico.width ? ico.width : 256; 65 | height_ = ico.height ? ico.width: 256; 66 | 67 | std::size_t file_pos {0}; 68 | bmp_data bmp; 69 | read_bmp_info_header(input, bmp, file_pos); 70 | 71 | if(bmp.width != width_ || (bmp.height != height_ && bmp.height != 2 * height_)) 72 | throw std::runtime_error{"Error reading ICO / CUR: size mismatch"}; 73 | 74 | set_size(width_, height_); 75 | 76 | bool has_and_mask = bmp.height == ico.height * 2; 77 | if(has_and_mask) 78 | bmp.height = ico.height; 79 | 80 | // get XOR mask 81 | read_bmp_data(input, bmp, file_pos, image_data_); 82 | 83 | // get & apply AND mask 84 | if(has_and_mask && bmp.bpp != 32) 85 | { 86 | bmp.bpp = 1; 87 | bmp.palette = {Color{0, 0, 0, 0xFF}, Color{0, 0, 0, 0x00}}; 88 | auto and_mask = image_data_; 89 | 90 | read_bmp_data(input, bmp, file_pos, and_mask); 91 | 92 | for(std::size_t row = 0; row < height_; ++row) 93 | { 94 | for(std::size_t col = 0; col < width_; ++col) 95 | image_data_[row][col].a &= and_mask[row][col].a; 96 | } 97 | } 98 | } 99 | catch(std::ios_base::failure&) 100 | { 101 | if(input.bad()) 102 | throw std::runtime_error{"Error reading ICO / CUR: could not read file"}; 103 | else 104 | throw std::runtime_error{"Error reading ICO / CUR: unexpected end of file"}; 105 | } 106 | } 107 | 108 | 109 | enum class Ico_type: std::uint16_t {ico = 1, cur = 2}; 110 | 111 | void ico_write_common(std::ostream & out, const Image & img, bool invert, Ico_type ico_type) 112 | { 113 | if(img.get_width() > 256 || img.get_height() > 256) 114 | throw std::runtime_error{"Image dimensions (" + std::to_string(img.get_width()) + "x" + std::to_string(img.get_height()) + ") exceed max CUR/ICO size (256x256)"}; 115 | 116 | // generate either png or bmp data 117 | std::ostringstream image_data; 118 | #ifdef PNG_FOUND 119 | if(ico_type == Ico_type::ico && (img.get_width() > 48 || img.get_height() > 48)) 120 | { 121 | Png::write(image_data, img, invert); 122 | } 123 | else 124 | { 125 | #endif 126 | write_bmp_info_header(image_data, img.get_width(), img.get_height(), false, true); 127 | write_bmp_data(image_data, img, invert); 128 | #ifdef PNG_FOUND 129 | } 130 | #endif 131 | auto img_data_buf = image_data.str(); // TODO: c++20 adds a view method. use that to prevent this copy 132 | 133 | // ICONDIR struct 134 | writeb(out, std::uint16_t{0}); // reserved. must be 0 135 | writeb(out, static_cast(ico_type)); // ico / cur type 136 | writeb(out, std::uint16_t{1}); // number of images in the file 137 | 138 | // ICONDIRENTRY struct 139 | writeb(out, img.get_width() >= 256 ? std::uint8_t{0} : static_cast(img.get_width())); // width 140 | writeb(out, img.get_height() >= 256 ? std::uint8_t{0} : static_cast(img.get_height())); // height 141 | writeb(out, std::uint8_t{0}); // # of palette colors 142 | writeb(out, std::uint8_t{0}); // reserved. must be 0 143 | if(ico_type == Ico_type::ico) 144 | { 145 | writeb(out, std::uint16_t{1}); // # of color planes 146 | writeb(out, std::uint16_t{32}); // bpp 147 | } 148 | else // if(ico_type == Ico_type::cur) 149 | { 150 | writeb(out, std::uint16_t{0}); // cursor hotspot x coord (UL corner) 151 | writeb(out, std::uint16_t{0}); // cursor hotspot y coord 152 | } 153 | writeb(out, static_cast(std::size(img_data_buf))); // image data size 154 | writeb(out, std::uint32_t{6 + 16}); //offset of image data. ICONDIR (6 bytes) + 1x ICONDIRENTRY (16 bytes); 155 | 156 | out.write(std::data(img_data_buf), std::size(img_data_buf)); 157 | } 158 | 159 | void Ico::write_cur(std::ostream & out, const Image & img, bool invert) 160 | { 161 | ico_write_common(out, img, invert, Ico_type::cur); 162 | } 163 | void Ico::write_ico(std::ostream & out, const Image & img, bool invert) 164 | { 165 | ico_write_common(out, img, invert, Ico_type::ico); 166 | } 167 | -------------------------------------------------------------------------------- /codecs/ico.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ICO_HPP 2 | #define ICO_HPP 3 | 4 | #include "image.hpp" 5 | 6 | inline bool is_ico(const Image::Header & header) 7 | { 8 | const std::array ico_header {0x00, 0x00, 0x01, 0x00}; 9 | const std::array cur_header {0x00, 0x00, 0x02, 0x00}; 10 | return std::equal(std::begin(ico_header), std::end(ico_header), std::begin(header), Image::header_cmp) || 11 | std::equal(std::begin(cur_header), std::end(cur_header), std::begin(header), Image::header_cmp); 12 | } 13 | 14 | class Ico final: public Image 15 | { 16 | public: 17 | Ico() = default; 18 | void open(std::istream & input, const Args & args) override; 19 | 20 | static void write_cur(std::ostream & out, const Image & img, bool invert); 21 | static void write_ico(std::ostream & out, const Image & img, bool invert); 22 | }; 23 | #endif // ICO_HPP 24 | -------------------------------------------------------------------------------- /codecs/image.hpp: -------------------------------------------------------------------------------- 1 | #ifndef IMAGE_HPP 2 | #define IMAGE_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "../args.hpp" 12 | #include "../color.hpp" 13 | #include "exif.hpp" 14 | 15 | struct Early_exit: public std::exception 16 | { 17 | const char * what() const noexcept; 18 | }; 19 | 20 | // set to the size of the longest magic number 21 | constexpr std::size_t max_header_len = 12; // 12 bytes needed to identify JPEGs 22 | 23 | class Image 24 | { 25 | public: 26 | Image() = default; 27 | Image(std::size_t w, std::size_t h) { set_size(w, h); } 28 | 29 | virtual ~Image() = default; 30 | Image(const Image &) = default; 31 | Image & operator=(const Image &) = default; 32 | Image(Image &&) = default; 33 | Image & operator=(Image &&) = default; 34 | 35 | const std::vector & operator[](std::size_t i) const 36 | { 37 | return image_data_[i]; 38 | } 39 | std::vector & operator[](std::size_t i) 40 | { 41 | return image_data_[i]; 42 | } 43 | std::size_t get_width() const { return width_; } 44 | std::size_t get_height() const { return height_; } 45 | void set_size(std::size_t w, std::size_t h); 46 | 47 | using Header = std::array; 48 | static bool header_cmp(unsigned char a, char b); 49 | static std::vector read_input_to_memory(std::istream & input); 50 | 51 | Image scale(std::size_t new_width, std::size_t new_height) const; 52 | void transpose_image(exif::Orientation orientation); 53 | 54 | std::vector generate_palette(std::size_t num_colors, bool gif_transparency = false) const; 55 | std::vector generate_and_apply_palette(std::size_t num_colors, bool gif_transparency = false); 56 | void dither(const std::function & palette_fun); 57 | template void dither(Iter palette_start, Iter palette_end); 58 | 59 | virtual void open(std::istream & input, const Args & args); 60 | void convert(const Args & args) const; 61 | 62 | virtual void handle_extra_args(const Args & args); 63 | virtual bool supports_multiple_images() const; 64 | virtual bool supports_subimages() const; 65 | virtual bool supports_animation() const; 66 | 67 | virtual std::size_t num_images() const; 68 | virtual std::size_t num_frames() const; 69 | virtual const Image & get_image(std::size_t image_no) const; 70 | virtual const Image & get_frame(std::size_t frame_no) const; 71 | virtual std::chrono::duration get_frame_delay(std::size_t image_no) const; 72 | 73 | char * row_buffer(std::size_t row); 74 | const char * row_buffer(std::size_t row) const; 75 | 76 | void swap_image_data(Image & other); 77 | void copy_image_data(const Image & other); 78 | void move_image_data(Image & other); 79 | 80 | protected: 81 | 82 | std::size_t width_{0}; 83 | std::size_t height_{0}; 84 | std::vector> image_data_; 85 | 86 | bool this_is_first_image_ {true}; 87 | std::vector images_; 88 | std::vector> frame_delays_; 89 | std::chrono::duration default_frame_delay_ {std::chrono::milliseconds{25}}; 90 | }; 91 | 92 | [[nodiscard]] std::unique_ptr get_image_data(const Args & args); 93 | 94 | template 95 | void Image::dither(Iter palette_start, Iter palette_end) 96 | { 97 | dither([palette_start, palette_end](const Color & c) 98 | { 99 | return *std::min_element(palette_start, palette_end, [c](const Color & a, const Color & b) 100 | { 101 | return color_dist2(a, c) < color_dist2(b, c); 102 | }); 103 | }); 104 | } 105 | 106 | #endif // IMAGE_HPP 107 | -------------------------------------------------------------------------------- /codecs/jp2.hpp: -------------------------------------------------------------------------------- 1 | #ifndef JP2_HPP 2 | #define JP2_HPP 3 | 4 | #include "image.hpp" 5 | 6 | #include 7 | 8 | inline bool is_jp2(const Image::Header & header) 9 | { 10 | const std::array jp2_header = {0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20, 0x0D, 0x0A, 0x87, 0x0A}; 11 | const std::array old_jp2_header = {0x0D, 0x0A, 0x87, 0x0A}; 12 | return std::equal(std::begin(jp2_header), std::end(jp2_header), std::begin(header), Image::header_cmp) || 13 | std::equal(std::begin(old_jp2_header), std::end(old_jp2_header), std::begin(header), Image::header_cmp); 14 | } 15 | inline bool is_jpx(const Image::Header & header) 16 | { 17 | const std::array jpx_header = {0xFF, 0x4F, 0xFF, 0x51}; 18 | return std::equal(std::begin(jpx_header), std::end(jpx_header), std::begin(header), Image::header_cmp); 19 | } 20 | 21 | #ifdef JP2_FOUND 22 | class Jp2 final: public Image 23 | { 24 | public: 25 | enum class Type {JP2, JPX, JPT}; 26 | Jp2(Type type): type_{type} {} 27 | void open(std::istream & input, const Args & args) override; 28 | 29 | static void write(std::ostream & out, const Image & img, bool invert); 30 | 31 | private: 32 | Type type_; 33 | }; 34 | #endif 35 | #endif // JP2_HPP 36 | -------------------------------------------------------------------------------- /codecs/jp2_color.hpp: -------------------------------------------------------------------------------- 1 | #ifndef JP2_COLOR_HPP 2 | #define JP2_COLOR_HPP 3 | 4 | #include 5 | 6 | bool color_sycc_to_rgb(opj_image_t * img); 7 | bool color_cmyk_to_rgb(opj_image_t * image); 8 | bool color_esycc_to_rgb(opj_image_t * image); 9 | 10 | #endif // JP2_COLOR_HPP 11 | -------------------------------------------------------------------------------- /codecs/jpeg.hpp: -------------------------------------------------------------------------------- 1 | #ifndef JPEG_HPP 2 | #define JPEG_HPP 3 | 4 | #include "image.hpp" 5 | 6 | inline bool is_jpeg(const Image::Header & header) 7 | { 8 | const std::array jpeg_header1 = {0xFF, 0xD8, 0xFF, 0xDB}; 9 | const std::array jpeg_header2_1 = {0xFF, 0xD8, 0xFF, 0xE0}; 10 | const std::array jpeg_header2_2 = {0x4A, 0x46, 0x49, 0x46, 0x00}; // there are 2 "don't care" bytes between these 11 | const std::array jpeg_header3 = {0xFF, 0xD8, 0xFF, 0xEE}; 12 | const std::array jpeg_header4_1 = {0xFF, 0xD8, 0xFF, 0xE1}; 13 | const std::array jpeg_header4_2 = {0x45, 0x78, 0x69, 0x66, 0x00, 0x00}; // there are 2 "don't care" bytes between these 14 | 15 | return std::equal(std::begin(jpeg_header1), std::end(jpeg_header1), std::begin(header), Image::header_cmp) 16 | || (std::equal(std::begin(jpeg_header2_1), std::end(jpeg_header2_1), std::begin(header), Image::header_cmp) 17 | && std::equal(std::begin(jpeg_header2_2), std::end(jpeg_header2_2), std::begin(header) + std::size(jpeg_header2_1) + 2, Image::header_cmp)) 18 | || std::equal(std::begin(jpeg_header3), std::end(jpeg_header3), std::begin(header), Image::header_cmp) 19 | || (std::equal(std::begin(jpeg_header4_1), std::end(jpeg_header4_1), std::begin(header), Image::header_cmp) 20 | && std::equal(std::begin(jpeg_header4_2), std::end(jpeg_header4_2), std::begin(header) + std::size(jpeg_header4_1) + 2, Image::header_cmp)); 21 | } 22 | 23 | #ifdef JPEG_FOUND 24 | class Jpeg final: public Image 25 | { 26 | public: 27 | Jpeg() = default; 28 | void open(std::istream & input, const Args & args) override; 29 | 30 | bool supports_multiple_images() const override { return true; } 31 | 32 | static void write(std::ostream & out, const Image & img, unsigned char bg, bool invert); 33 | }; 34 | #endif 35 | #endif // JPEG_HPP 36 | -------------------------------------------------------------------------------- /codecs/jxl.cpp: -------------------------------------------------------------------------------- 1 | #include "jxl.hpp" 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | void Jxl::open(std::istream & input, const Args &) 11 | { 12 | auto decoder = JxlDecoderMake(nullptr); 13 | if(!decoder) 14 | throw std::runtime_error {"Could not create JPEG XL decoder"}; 15 | 16 | JxlPixelFormat format 17 | { 18 | 4, // num_channels 19 | JXL_TYPE_UINT8, // data_type 20 | JXL_LITTLE_ENDIAN, // endianness 21 | 0 // align 22 | }; 23 | 24 | JxlBasicInfo info{}; 25 | std::array input_buffer{}; 26 | std::size_t input_size {0}; 27 | std::vector buffer{}; 28 | 29 | if(JxlDecoderSubscribeEvents(decoder.get(), JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) 30 | throw std::runtime_error {"Error subscibing to JPEG XL events"}; 31 | 32 | // NOTE: libjxl automatically will apply rotation if needed, so we don't have to mess about with Exif ourselves. Yay! 33 | // If for some reason we ever do need to get it, we add JXL_DEC_BOX to the JxlDecoderSubscribeEvents call, add JXL_DEC_BOX and JXL_DEC_BOX_NEED_MORE_OUTPUT to the switch below, 34 | // For JXL_DEC_BOX, we check for Exif with JxlDecoderGetBoxType and set a buffer with JxlDecoderSetBoxBuffer 35 | // For JXL_DEC_BOX_NEED_MORE_OUTPUT, we get the number of unused bytes in our buffer with JxlDecoderReleaseBoxBuffer, increase the buffer size, call JxlDecoderSetBoxBuffer again with an offset to where it had left off 36 | // After we get SUCCESS, we need to check the offset at the start of the exif buffer (32-bit big endian number, usually 0), drop that many + 4 bytes (for the offset), and then prepend "Exif\0\0" to the front. libexif will take it from there 37 | // JxlDecoderReleaseBoxBuffer doesn't need to be called before the decoder is destroyed - it will be automatically cleaned by the decode destructor 38 | 39 | for(bool decoding = true; decoding;) 40 | { 41 | auto status = JxlDecoderProcessInput(decoder.get()); 42 | 43 | switch(status) 44 | { 45 | case JXL_DEC_SUCCESS: 46 | case JXL_DEC_FULL_IMAGE: // right now we're quitting becuase we don't care about anything after the full image, but if we did, we'd want to handle JXL_DEC_FULL_IMAGE separately from JXL_DEC_SUCCESS 47 | // This would come up when we're ready to handle multiple images or animations (In which case this signals the end of a single image/frame, and more remain) 48 | decoding = false; 49 | break; 50 | 51 | case JXL_DEC_BASIC_INFO: 52 | if(JxlDecoderGetBasicInfo(decoder.get(), &info) != JXL_DEC_SUCCESS) 53 | throw std::runtime_error {"Error decoding JPEG XL data. Unable to read JPEG XL basic info"}; 54 | break; 55 | 56 | case JXL_DEC_NEED_MORE_INPUT: 57 | { 58 | if(input.eof()) 59 | throw std::runtime_error {"Error decoding JPEG XL image: Unexpected end of input"}; 60 | 61 | auto remaining = JxlDecoderReleaseInput(decoder.get()); 62 | 63 | if(remaining) 64 | { 65 | auto leftovers = std::vector(std::begin(input_buffer) + input_size - remaining, std::begin(input_buffer) + input_size); 66 | std::copy(std::begin(leftovers), std::end(leftovers), std::begin(input_buffer)); 67 | } 68 | 69 | input.read(reinterpret_cast(std::data(input_buffer)) + remaining, std::size(input_buffer) - remaining); 70 | if(input.bad()) 71 | throw std::runtime_error {"Error reading Jpeg XL file"}; 72 | input_size = input.gcount() + remaining; 73 | 74 | if(JxlDecoderSetInput(decoder.get(), std::data(input_buffer), input_size) != JXL_DEC_SUCCESS) 75 | throw std::runtime_error {"Error supplying JPEG XL input data"}; 76 | 77 | if(input.eof()) 78 | JxlDecoderCloseInput(decoder.get()); 79 | 80 | break; 81 | } 82 | case JXL_DEC_NEED_IMAGE_OUT_BUFFER: 83 | { 84 | std::size_t buffer_size {0}; 85 | if(JxlDecoderImageOutBufferSize(decoder.get(), &format, &buffer_size) != JXL_DEC_SUCCESS) 86 | throw std::runtime_error {"Error getting JPEG XL decompressed size"}; 87 | 88 | buffer.resize(buffer_size); 89 | 90 | if(JxlDecoderSetImageOutBuffer(decoder.get(), &format, std::data(buffer), std::size(buffer)) != JXL_DEC_SUCCESS) 91 | throw std::runtime_error {"Error setting JPEG XL buffer"}; 92 | 93 | break; 94 | } 95 | 96 | case JXL_DEC_ERROR: 97 | throw std::runtime_error {"Error decoding JPEG XL image"}; 98 | 99 | default: 100 | throw std::runtime_error {"Unhandled status for JPEG XL ProcessInput: " + std::to_string(status)}; 101 | } 102 | } 103 | 104 | set_size(info.xsize, info.ysize); 105 | 106 | for(std::size_t row = 0; row < height_; ++row) 107 | { 108 | for(std::size_t col = 0; col < width_; ++col) 109 | { 110 | auto pixel = std::data(buffer) + (row * width_ + col) * 4; 111 | image_data_[row][col] = Color{ pixel[0], pixel[1], pixel[2], pixel[3] }; 112 | } 113 | } 114 | } 115 | 116 | void Jxl::write(std::ostream & out, const Image & img, bool invert) 117 | { 118 | auto encoder = JxlEncoderMake(nullptr); 119 | if(!encoder) 120 | throw std::runtime_error {"Could not create JPEG XL encoder"}; 121 | 122 | JxlPixelFormat format 123 | { 124 | 4, // num_channels 125 | JXL_TYPE_UINT8, // data_type 126 | JXL_LITTLE_ENDIAN, // endianness 127 | 0 // align 128 | }; 129 | 130 | JxlBasicInfo info {}; 131 | JxlEncoderInitBasicInfo(&info); 132 | info.xsize = static_cast(img.get_width()); 133 | info.ysize = static_cast(img.get_height()); 134 | info.num_color_channels = 3; 135 | info.num_extra_channels = 1; 136 | info.bits_per_sample = 8; 137 | info.alpha_bits = 8; 138 | 139 | if(JxlEncoderSetBasicInfo(encoder.get(), &info) != JXL_ENC_SUCCESS) 140 | throw std::runtime_error {"Could not set JPEG XL basic info"}; 141 | 142 | JxlColorEncoding color; 143 | JxlColorEncodingSetToSRGB(&color, false); 144 | if(JxlEncoderSetColorEncoding(encoder.get(), &color) != JXL_ENC_SUCCESS) 145 | throw std::runtime_error {"Could not set JPEG XL color encoding"}; 146 | 147 | auto encoder_opts = JxlEncoderFrameSettingsCreate(encoder.get(), nullptr); 148 | if(!encoder_opts) 149 | throw std::runtime_error {"Could not create JPEG XL encoder options"}; 150 | 151 | std::vector data(img.get_width() * img.get_height() * 4); 152 | 153 | for(std::size_t row = 0; row < img.get_height(); ++row) 154 | { 155 | for(std::size_t col = 0; col < img.get_width(); ++col) 156 | { 157 | for(std::size_t i = 0; i < 4; ++i) 158 | { 159 | if(invert && i < 3) 160 | data[(row * img.get_width() + col) * 4 + i] = 255 - img[row][col][i]; 161 | else 162 | data[(row * img.get_width() + col) * 4 + i] = img[row][col][i]; 163 | } 164 | } 165 | } 166 | 167 | if(JxlEncoderAddImageFrame(encoder_opts, &format, std::data(data), std::size(data) * sizeof(decltype(data)::value_type)) != JXL_ENC_SUCCESS) 168 | throw std::runtime_error {"Could not add JPEG XL image data"}; 169 | 170 | JxlEncoderCloseInput(encoder.get()); 171 | 172 | std::array output_buffer; 173 | 174 | for(bool encoding = true; encoding;) 175 | { 176 | std::uint8_t * next_out = std::data(output_buffer); 177 | std::size_t avail_out = std::size(output_buffer); 178 | 179 | auto status = JxlEncoderProcessOutput(encoder.get(), &next_out, &avail_out); 180 | switch(status) 181 | { 182 | case JXL_ENC_SUCCESS: 183 | out.write(reinterpret_cast(std::data(output_buffer)), next_out - std::data(output_buffer)); 184 | encoding = false; 185 | break; 186 | 187 | case JXL_ENC_NEED_MORE_OUTPUT: 188 | out.write(reinterpret_cast(std::data(output_buffer)), next_out - std::data(output_buffer)); 189 | break; 190 | 191 | default: 192 | throw std::runtime_error {"Could not encode JPEG XL data"}; 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /codecs/jxl.hpp: -------------------------------------------------------------------------------- 1 | #ifndef JXL_HPP 2 | #define JXL_HPP 3 | 4 | #include "image.hpp" 5 | 6 | inline bool is_jxl(const Image::Header & header) 7 | { 8 | const std::array jxl_header1 = {0xFF, 0x0A}; 9 | const std::array jxl_header2 = {0x0A, 0x04, 0x42, 0xD2, 0xD5, 0x4E}; 10 | const std::array jxl_header3 = {0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 11 | 0x0D, 0x0A, 0x87, 0x0A}; 12 | 13 | return std::equal(std::begin(jxl_header1), std::end(jxl_header1), std::begin(header), Image::header_cmp) 14 | || std::equal(std::begin(jxl_header2), std::end(jxl_header2), std::begin(header), Image::header_cmp) 15 | || std::equal(std::begin(jxl_header3), std::end(jxl_header3), std::begin(header), Image::header_cmp); 16 | } 17 | 18 | #ifdef JXL_FOUND 19 | class Jxl final: public Image 20 | { 21 | public: 22 | Jxl() = default; 23 | void open(std::istream & input, const Args & args) override; 24 | 25 | static void write(std::ostream & out, const Image & img, bool invert); 26 | }; 27 | #endif 28 | #endif // JXL_HPP 29 | -------------------------------------------------------------------------------- /codecs/mcmap.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MCMAP_HPP 2 | #define MCMAP_HPP 3 | 4 | #include "image.hpp" 5 | 6 | #ifdef ZLIB_FOUND 7 | class MCMap final: public Image 8 | { 9 | public: 10 | MCMap() = default; 11 | void open(std::istream & input, const Args & args) override; 12 | 13 | static void write(std::ostream & out, const Image & img, unsigned char bg, bool invert); 14 | }; 15 | #endif 16 | #endif // MCMAP_HPP 17 | -------------------------------------------------------------------------------- /codecs/mng.cpp: -------------------------------------------------------------------------------- 1 | #include "mng.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | 12 | class Libmng 13 | { 14 | private: 15 | mng_handle mng_ {nullptr}; 16 | 17 | static mng_ptr mng_alloc(mng_size_t len) noexcept { return std::calloc(1u, len); } 18 | static void mng_free(mng_ptr p, mng_size_t) noexcept { std::free(p); } 19 | 20 | public: 21 | 22 | Libmng(void * user) 23 | { 24 | mng_ = mng_initialize(user, mng_alloc, mng_free, nullptr); 25 | if(!mng_) 26 | { 27 | throw std::runtime_error{"Error initializing libmng"}; 28 | } 29 | } 30 | ~Libmng() 31 | { 32 | if(mng_) 33 | mng_cleanup(&mng_); 34 | } 35 | 36 | Libmng(const Libmng &) = delete; 37 | Libmng & operator=(const Libmng &) = delete; 38 | 39 | Libmng(Libmng && other): 40 | mng_{std::move(other.mng_)} 41 | { 42 | other.mng_ = nullptr; 43 | } 44 | 45 | Libmng & operator=(Libmng && other) 46 | { 47 | if(this != &other) 48 | { 49 | mng_ = std::move(other.mng_); 50 | other.mng_ = nullptr; 51 | } 52 | return *this; 53 | } 54 | 55 | [[noreturn]] void throw_error(const std::string & where = ": ") 56 | { 57 | mng_int8 severity; 58 | mng_chunkid chunk_name; 59 | mng_uint32 chunk_seq; 60 | mng_int32 extra1, extra2; 61 | mng_pchar text; 62 | 63 | mng_getlasterror(mng_, &severity, &chunk_name, &chunk_seq, &extra1, &extra2, &text); 64 | throw std::runtime_error{"libmng error" + where + (text ? text : "Unspecified error")}; 65 | } 66 | 67 | operator mng_handle() { return mng_; } 68 | operator mng_data_struct const *() const { return mng_; } 69 | }; 70 | 71 | void Mng::open(std::istream & input, const Args &) 72 | { 73 | struct Mng_info 74 | { 75 | Mng & mng; 76 | unsigned int width{0u}, height{0u}; 77 | std::istream & input; 78 | mng_uint32 faked_timer {0}; 79 | 80 | bool hit_mend {false}; 81 | 82 | Mng_info(Mng & mng, std::istream & input): 83 | mng{mng}, 84 | input{input} 85 | {} 86 | }; 87 | auto mng_info = Mng_info{*this, input}; 88 | 89 | auto mng = std::make_unique(&mng_info); 90 | 91 | if(mng_set_canvasstyle(*mng, MNG_CANVAS_RGBA8) != MNG_NOERROR) 92 | mng->throw_error(" on setting canvas: "); 93 | 94 | if(mng_setcb_openstream(*mng, [](mng_handle) noexcept -> mng_bool { return MNG_TRUE; }) != MNG_NOERROR) 95 | mng->throw_error(" on setting stream_cb: "); 96 | if(mng_setcb_closestream(*mng, [](mng_handle) noexcept -> mng_bool { return MNG_TRUE; }) != MNG_NOERROR) 97 | mng->throw_error(" on setting stream_cb: "); 98 | if(mng_setcb_readdata(*mng, [](mng_handle handle, mng_ptr buf, mng_uint32 size, mng_uint32p bytes_read) noexcept -> mng_bool 99 | { 100 | auto mng_info = reinterpret_cast(mng_get_userdata(handle)); 101 | mng_info->input.read(reinterpret_cast(buf), size); 102 | *bytes_read = mng_info->input.gcount(); 103 | return MNG_TRUE; 104 | }) != MNG_NOERROR) 105 | mng->throw_error(" on setting read_cb: "); 106 | 107 | if(mng_setcb_processheader(*mng, [](mng_handle handle, mng_uint32 width, mng_uint32 height) -> mng_bool 108 | { 109 | auto mng_info = reinterpret_cast(mng_get_userdata(handle)); 110 | 111 | mng_info->width = width; 112 | mng_info->height = height; 113 | mng_info->mng.images_.back().set_size(width, height); 114 | 115 | return MNG_TRUE; 116 | }) != MNG_NOERROR) 117 | mng->throw_error(" setting header callback: "); 118 | 119 | if(mng_setcb_gettickcount(*mng, [](mng_handle handle) -> mng_uint32 120 | { 121 | auto mng_info = reinterpret_cast(mng_get_userdata(handle)); 122 | 123 | return mng_info->faked_timer; 124 | }) != MNG_NOERROR) 125 | mng->throw_error(" setting gettickcount callback: "); 126 | 127 | if(mng_setcb_settimer(*mng, [](mng_handle handle, mng_uint32 duration) -> mng_bool 128 | { 129 | auto mng_info = reinterpret_cast(mng_get_userdata(handle)); 130 | 131 | mng_info->faked_timer += duration; 132 | 133 | return MNG_TRUE; 134 | }) != MNG_NOERROR) 135 | mng->throw_error(" setting setttimer callback: "); 136 | 137 | if(mng_setcb_getcanvasline(*mng, [](mng_handle handle, mng_uint32 row) -> mng_ptr 138 | { 139 | auto mng_info = reinterpret_cast(mng_get_userdata(handle)); 140 | 141 | return reinterpret_cast(mng_info->mng.images_.back().row_buffer(row)); 142 | }) != MNG_NOERROR) 143 | mng->throw_error(" setting row callback: "); 144 | 145 | if(mng_setcb_refresh(*mng, [](mng_handle handle, mng_uint32, mng_uint32, mng_uint32, mng_uint32) -> mng_bool 146 | { 147 | auto mng_info = reinterpret_cast(mng_get_userdata(handle)); 148 | 149 | mng_info->mng.images_.emplace_back(mng_info->mng.images_.back()); 150 | 151 | return MNG_TRUE; 152 | }) != MNG_NOERROR) 153 | mng->throw_error(" setting refresh_callback "); 154 | 155 | images_.emplace_back(); 156 | if(mng_read(*mng) != MNG_NOERROR) 157 | mng->throw_error(" reading input: "); 158 | 159 | if(mng_setcb_processmend(*mng, [](mng_handle handle, mng_uint32, mng_uint32) -> mng_bool 160 | { 161 | auto mng_info = reinterpret_cast(mng_get_userdata(handle)); 162 | 163 | mng_info->hit_mend = true; 164 | 165 | return MNG_TRUE; 166 | }) != MNG_NOERROR) 167 | mng->throw_error(" setting mend callback "); 168 | 169 | auto disp_status = mng_display(*mng); 170 | while(disp_status == MNG_NEEDTIMERWAIT) 171 | { 172 | disp_status = mng_display_resume(*mng); 173 | if(mng_info.hit_mend) 174 | { 175 | disp_status = MNG_NOERROR; 176 | break; 177 | } 178 | } 179 | if(disp_status != MNG_NOERROR) 180 | mng->throw_error(" on retrieving frames: "); 181 | 182 | 183 | default_frame_delay_ = decltype(default_frame_delay_){mng_get_ticks(*mng) ? mng_get_ticks(*mng) / 1000.0f : 1.0f / 30.0f}; 184 | 185 | mng.reset(); 186 | move_image_data(images_.front()); 187 | images_.erase(std::begin(images_)); 188 | } 189 | -------------------------------------------------------------------------------- /codecs/mng.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MNG_HPP 2 | #define MNG_HPP 3 | 4 | #include "image.hpp" 5 | 6 | inline bool is_mng(const Image::Header & header) 7 | { 8 | const std::array mng_header = {0x8A, 0x4D, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; 9 | const std::array jng_header = {0x8B, 0x4A, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; 10 | 11 | return std::equal(std::begin(mng_header), std::end(mng_header), std::begin(header), Image::header_cmp) 12 | || std::equal(std::begin(jng_header), std::end(jng_header), std::begin(header), Image::header_cmp); 13 | } 14 | 15 | #ifdef MNG_FOUND 16 | class Mng final: public Image 17 | { 18 | public: 19 | Mng() = default; 20 | void open(std::istream & input, const Args & args) override; 21 | 22 | bool supports_multiple_images() const override { return true; } 23 | bool supports_animation() const override { return true; } 24 | }; 25 | #endif 26 | #endif // MNG_HPP 27 | -------------------------------------------------------------------------------- /codecs/motologo.cpp: -------------------------------------------------------------------------------- 1 | #include "motologo.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include "binio.hpp" 11 | #include "sub_args.hpp" 12 | 13 | /* File format notes 14 | * 15 | * Header: 16 | * magic: 9 byte str: 'MotoLogo\0' 17 | * header_size: LE uint32_t 18 | * 32 byte entries (count: (header_size - 13) / 32) 19 | * name: 24 byte null-terminated str name 20 | * offset: LE uint32_t (from file start) 21 | * size: LE uint32_t 22 | * 23 | * images: 24 | * aligned to 512 bytes, padded with \xff 25 | * magic: 8 byte str: 'MotoRun\0' 26 | * width: BE uint16_t 27 | * height: BE uint16_t 28 | * pixel_data: RLE compressed BGR (count: size - 16) 29 | * 30 | * RLE compression scheme: 31 | * count: BE uint16_t 32 | * R000CCCC CCCCCCCC 33 | * C: pixel count 34 | * R: repeat 35 | * 36 | * R = count & 0x8000 37 | * C = count & 0x0FFF 38 | * if(R) 39 | * next 3 bytes (BGR) repeated C times 40 | * else 41 | * next C * 3 bytes are raw (BGR) pixels 42 | * 43 | * repeat count seems to be limited to current line 44 | */ 45 | 46 | constexpr auto magic_size = 9u; 47 | constexpr auto dir_entry_size = 32u; 48 | constexpr auto name_size = 24u; 49 | constexpr auto image_magic_size = 8u; 50 | 51 | void MotoLogo::open(std::istream & input, const Args &) 52 | { 53 | input.exceptions(std::ios_base::badbit | std::ios_base::failbit); 54 | try 55 | { 56 | std::size_t pos {0}; 57 | 58 | // header 59 | input.ignore(magic_size); // Magic 60 | std::uint32_t directory_size; 61 | readb(input, directory_size, std::endian::little); 62 | 63 | pos += magic_size + sizeof(directory_size); 64 | 65 | const auto num_images = (directory_size - magic_size - sizeof(directory_size)) / dir_entry_size; 66 | auto name_lookup = std::unordered_map{}; 67 | auto locations = std::vector >{}; 68 | 69 | for(auto i = 0u; i < num_images; ++i) 70 | { 71 | std::uint32_t offset, size; 72 | auto name = readstr(input, name_size); 73 | readb(input, offset, std::endian::little); 74 | readb(input, size, std::endian::little); 75 | 76 | pos += dir_entry_size; 77 | 78 | name.resize(name.find_first_of('\0')); 79 | name_lookup[name] = i; 80 | locations.emplace_back(offset, size); 81 | 82 | if(list_) 83 | std::cout<<" "<{}; 98 | input.read(std::data(image_magic), std::size(image_magic)); pos += std::size(image_magic); 99 | for(auto j = 0u; j < image_magic_size; ++j) 100 | { 101 | if(image_magic[j] != expected_image_magic[j]) 102 | throw std::runtime_error{"Error reading MotoLogo: Bad magic number on Image"}; 103 | } 104 | 105 | std::uint16_t width, height; 106 | readb(input, width, std::endian::big); 107 | readb(input, height, std::endian::big); 108 | pos += sizeof(std::uint16_t) * 2; 109 | 110 | auto & img = images_.emplace_back(width, height); 111 | 112 | auto row = 0u; 113 | auto write_pix = [&row, col = 0u, &img](std::uint8_t r, std::uint8_t g, std::uint8_t b) mutable 114 | { 115 | if(row >= img.get_height()) 116 | throw std::runtime_error{"MotoLogo image data out of range"}; 117 | 118 | auto & p = img[row][col]; 119 | p.r = r; 120 | p.g = g; 121 | p.b = b; 122 | p.a = 255u; 123 | 124 | if(++col == img.get_width()) 125 | { 126 | col = 0u; 127 | ++row; 128 | } 129 | }; 130 | 131 | auto img_pos = 0u; 132 | auto check_pos = [&pos, &img_pos, size=size](unsigned int bytes = 1) { if(pos += bytes, img_pos += bytes > size) throw std::runtime_error{"MotoLogo image read past size"}; }; 133 | while(row < img.get_height()) 134 | { 135 | std::uint16_t count; 136 | readb(input, count, std::endian::big); check_pos(sizeof(std::uint16_t)); 137 | if(count & 0x7000u) 138 | throw std::runtime_error{"Error reading MotoLogo: bad RLE count"}; 139 | 140 | bool repeat = count & 0x8000u; 141 | count &= 0x0FFFu; 142 | 143 | std::uint8_t r{0}, g{0}, b{0}; 144 | if(repeat) 145 | { 146 | readb(input, b); check_pos(); 147 | readb(input, g); check_pos(); 148 | readb(input, r); check_pos(); 149 | 150 | for(auto i = 0u; i < count; ++i) 151 | write_pix(r, g, b); 152 | } 153 | else 154 | { 155 | for(auto i = 0u; i < count; ++i) 156 | { 157 | readb(input, b); check_pos(); 158 | readb(input, g); check_pos(); 159 | readb(input, r); check_pos(); 160 | write_pix(r, g, b); 161 | } 162 | } 163 | } 164 | } 165 | if(image_name_) 166 | { 167 | if(auto target = name_lookup.find(*image_name_); target != std::end(name_lookup)) 168 | copy_image_data(images_[target->second]); 169 | else 170 | throw std::runtime_error{"Error reading MotoLogo: requested image '" + *image_name_ + "' not found in MotoLogo file"}; 171 | } 172 | else 173 | { 174 | move_image_data(images_.front()); 175 | images_.erase(std::begin(images_)); 176 | } 177 | } 178 | catch(std::ios_base::failure & e) 179 | { 180 | if(input.bad()) 181 | throw std::runtime_error{"Error reading MotoLogo: could not read file"}; 182 | else 183 | throw std::runtime_error{"Error reading MotoLogo: unexpected end of file"}; 184 | } 185 | } 186 | 187 | void MotoLogo::handle_extra_args(const Args & args) 188 | { 189 | auto options = Sub_args{"MotoLogo"}; 190 | try 191 | { 192 | options.add_options() 193 | ("list-images", "list all image names contained in input file") 194 | ("image-name", "image name to extract", cxxopts::value(), "IMAGE_NAME"); 195 | 196 | auto sub_args = options.parse(args.extra_args); 197 | 198 | list_ = sub_args.count("list-images"); 199 | 200 | if(sub_args.count("image-name")) 201 | { 202 | if(args.image_no) 203 | throw std::runtime_error{options.help(args.help_text) + "\nCan't specify --image-name with --image-no"}; 204 | 205 | image_name_ = sub_args["image-name"].as(); 206 | } 207 | 208 | if(args.animate && args.image_no) 209 | throw std::runtime_error{options.help(args.help_text) + "\nCan't specify --image-no with --animate"}; 210 | } 211 | catch(const cxxopt_exception & e) 212 | { 213 | throw std::runtime_error{options.help(args.help_text) + '\n' + e.what()}; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /codecs/motologo.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MOTOLOGO_HPP 2 | #define MOTOLOGO_HPP 3 | 4 | #include "image.hpp" 5 | 6 | inline bool is_motologo(const Image::Header & header) 7 | { 8 | const std::array srf_header = {'M', 'o', 't', 'o', 'L', 'o', 'g', 'o', '\0'}; 9 | 10 | return std::equal(std::begin(srf_header), std::end(srf_header), std::begin(header), Image::header_cmp); 11 | } 12 | 13 | class MotoLogo final: public Image 14 | { 15 | public: 16 | MotoLogo() = default; 17 | void open(std::istream & input, const Args & args) override; 18 | 19 | void handle_extra_args(const Args & args) override; 20 | bool supports_multiple_images() const override { return true; } 21 | 22 | private: 23 | bool list_ {false}; 24 | std::optional image_name_; 25 | }; 26 | #endif // MOTOLOGO_HPP 27 | -------------------------------------------------------------------------------- /codecs/openexr.cpp: -------------------------------------------------------------------------------- 1 | #include "openexr.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | class OpenExr_reader: public Imf::IStream 12 | { 13 | public: 14 | 15 | OpenExr_reader(std::istream & input): 16 | Imf::IStream("memory"), 17 | data_{Image::read_input_to_memory(input)}, 18 | pos_{std::data(data_)} 19 | {} 20 | 21 | virtual bool isMemoryMapped() const override 22 | { 23 | return true; 24 | } 25 | 26 | virtual char * readMemoryMapped(int n) override 27 | { 28 | if(std::distance(std::data(data_), pos_ + n) > static_cast(std::size(data_))) 29 | throw std::runtime_error{"Attempted to read past end of EXR file"}; 30 | 31 | auto ret = pos_; 32 | pos_ += n; 33 | return reinterpret_cast(ret); 34 | } 35 | 36 | virtual bool read(char c[], int n) override 37 | { 38 | if(std::distance(std::data(data_), pos_ + n) > static_cast(std::size(data_))) 39 | throw std::runtime_error{"Attempted to read past end of EXR file"}; 40 | 41 | std::memcpy(c, pos_, n); 42 | pos_ += n; 43 | return true; 44 | } 45 | 46 | virtual std::uint64_t tellg() override 47 | { 48 | return std::distance(std::data(data_), pos_); 49 | } 50 | 51 | virtual void seekg(std::uint64_t pos) override 52 | { 53 | if(std::distance(std::data(data_), std::data(data_) + pos) > static_cast(std::size(data_))) 54 | throw std::runtime_error{"Attempted to read past end of EXR file"}; 55 | 56 | pos_ = std::data(data_) + pos; 57 | } 58 | 59 | private: 60 | std::vector data_; 61 | unsigned char * pos_; 62 | }; 63 | 64 | class OpenExr_writer: public Imf::OStream 65 | { 66 | public: 67 | 68 | OpenExr_writer(): 69 | Imf::OStream("memory") 70 | {} 71 | 72 | virtual void write(const char c[], int n) override 73 | { 74 | if(pos_ + n > std::size(data_)) 75 | data_.resize(pos_ + n); 76 | 77 | std::copy(c, c + n, std::begin(data_) + pos_); 78 | pos_ += n; 79 | } 80 | 81 | virtual std::uint64_t tellp() override 82 | { 83 | return pos_; 84 | } 85 | 86 | virtual void seekp(std::uint64_t pos) override 87 | { 88 | pos_ = pos; 89 | if(pos_ > std::size(data_)) 90 | data_.resize(pos_); 91 | } 92 | 93 | void output(std::ostream & out) 94 | { 95 | out.write(std::data(data_), std::size(data_)); 96 | } 97 | 98 | private: 99 | std::vector data_; 100 | std::size_t pos_ {0}; 101 | }; 102 | 103 | void OpenEXR::open(std::istream & input, const Args &) 104 | { 105 | try 106 | { 107 | OpenExr_reader reader{input}; 108 | Imf::RgbaInputFile file{reader}; 109 | 110 | const auto dimensions = file.dataWindow(); 111 | set_size(dimensions.max.x - dimensions.min.x + 1, dimensions.max.y - dimensions.min.y + 1); 112 | 113 | std::vector rowbuf(width_); 114 | for(std::size_t row = 0; row < height_; ++row) 115 | { 116 | file.setFrameBuffer(std::data(rowbuf) - dimensions.min.x - (dimensions.min.y + row) * width_, 1, width_); // no idea why the API will have the base pointer begfore the start of data 117 | file.readPixels(row + dimensions.min.y); 118 | 119 | for(std::size_t col = 0; col < width_; ++col) 120 | { 121 | image_data_[row][col].r = rowbuf[col].r * 255.0f; 122 | image_data_[row][col].g = rowbuf[col].g * 255.0f; 123 | image_data_[row][col].b = rowbuf[col].b * 255.0f; 124 | image_data_[row][col].a = rowbuf[col].a * 255.0f; 125 | } 126 | } 127 | } 128 | catch(Iex::BaseExc & e) 129 | { 130 | throw std::runtime_error{"Error reading EXR file: " + std::string{e.what()}}; 131 | } 132 | } 133 | 134 | void OpenEXR::write(std::ostream & out, const Image & img, bool invert) 135 | { 136 | try 137 | { 138 | OpenExr_writer writer; 139 | Imf::RgbaOutputFile file{writer, Imf::Header{static_cast(img.get_width()), static_cast(img.get_height())}, Imf::WRITE_RGBA}; 140 | 141 | std::vector rowbuf(img.get_width()); 142 | for(std::size_t row = 0; row < img.get_height(); ++row) 143 | { 144 | file.setFrameBuffer(std::data(rowbuf) - row * img.get_width(), 1, img.get_width()); 145 | file.writePixels(1); 146 | 147 | for(std::size_t col = 0; col < img.get_width(); ++col) 148 | { 149 | if(invert) 150 | { 151 | rowbuf[col].r = img[row][col].r / 255.0f; 152 | rowbuf[col].g = img[row][col].g / 255.0f; 153 | rowbuf[col].b = img[row][col].b / 255.0f; 154 | } 155 | else 156 | { 157 | rowbuf[col].r = 1.0f - img[row][col].r / 255.0f; 158 | rowbuf[col].g = 1.0f - img[row][col].g / 255.0f; 159 | rowbuf[col].b = 1.0f - img[row][col].b / 255.0f; 160 | } 161 | rowbuf[col].a = img[row][col].a / 255.0f; 162 | } 163 | } 164 | 165 | writer.output(out); 166 | } 167 | catch(Iex::BaseExc & e) 168 | { 169 | throw std::runtime_error{"Error reading EXR file: " + std::string{e.what()}}; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /codecs/openexr.hpp: -------------------------------------------------------------------------------- 1 | #ifndef OPENEXR_HPP 2 | #define OPENEXR_HPP 3 | 4 | #include "image.hpp" 5 | 6 | inline bool is_openexr(const Image::Header & header) 7 | { 8 | const std::array exr_header = {0x76, 0x2F, 0x31, 0x01}; 9 | 10 | return std::equal(std::begin(exr_header), std::end(exr_header), std::begin(header), Image::header_cmp); 11 | } 12 | 13 | #ifdef OpenEXR_FOUND 14 | class OpenEXR final: public Image 15 | { 16 | public: 17 | OpenEXR() = default; 18 | void open(std::istream & input, const Args & args) override; 19 | 20 | static void write(std::ostream & out, const Image & img, bool invert); 21 | }; 22 | #endif 23 | #endif // OPENEXR_HPP 24 | -------------------------------------------------------------------------------- /codecs/pcx.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PCX_HPP 2 | #define PCX_HPP 3 | 4 | #include "image.hpp" 5 | 6 | class Pcx final: public Image 7 | { 8 | public: 9 | Pcx() = default; 10 | void open(std::istream & input, const Args & args) override; 11 | 12 | static void write(std::ostream & out, const Image & img, unsigned char bg, bool invert); 13 | }; 14 | #endif // PCX_HPP 15 | -------------------------------------------------------------------------------- /codecs/pkmn_gen1.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PKMN_GEN1_HPP 2 | #define PKMN_GEN1_HPP 3 | 4 | #include 5 | 6 | #include "image.hpp" 7 | 8 | class Pkmn_gen1 final: public Image 9 | { 10 | public: 11 | Pkmn_gen1() = default; 12 | void open(std::istream & input, const Args & args) override; 13 | 14 | void handle_extra_args(const Args & args) override; 15 | 16 | static void write(std::ostream & out, const Image & img, bool invert); 17 | 18 | static const std::map> palettes; 19 | 20 | private: 21 | unsigned int override_tile_width_ {0}, override_tile_height_ {0}; 22 | bool check_overrun_ {true}; 23 | bool fixed_buffer_ {false}; 24 | 25 | inline static std::array palette_entries_; 26 | }; 27 | #endif // PKMN_GEN1_HPP 28 | -------------------------------------------------------------------------------- /codecs/pkmn_gen2.cpp: -------------------------------------------------------------------------------- 1 | #include "pkmn_gen2.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include "pkmn_gen1.hpp" 7 | #include "sub_args.hpp" 8 | 9 | constexpr auto tile_dims = 8u; 10 | constexpr auto tile_bytes = 16u; 11 | 12 | void process_cmd(std::istream & input, std::vector & decompressed, std::uint8_t cmd, std::size_t length) 13 | { 14 | switch(cmd) 15 | { 16 | case 0x0: // direct copy 17 | { 18 | for(auto i = 0u; i < length; ++i) 19 | decompressed.emplace_back(input.get()); 20 | return; 21 | } 22 | case 0x1: // byte fill 23 | { 24 | auto v = input.get(); 25 | for(auto i = 0u; i < length; ++i) 26 | decompressed.emplace_back(v); 27 | return; 28 | } 29 | case 0x2: // word fill 30 | { 31 | auto v1 = input.get(); 32 | auto v2 = input.get(); 33 | for(auto i = 0u; i < length; ++i) 34 | decompressed.emplace_back((i % 2 == 0) ? v1 : v2); 35 | return; 36 | } 37 | case 0x3: // zero fill 38 | { 39 | for(auto i = 0u; i < length; ++i) 40 | decompressed.emplace_back(0); 41 | return; 42 | } 43 | case 0x4: // repeat 44 | case 0x5: // bit-reverse repeat 45 | case 0x6: // backwards repeat 46 | { 47 | auto ay = static_cast(input.get()); 48 | auto a = ay >> 7; 49 | auto y = ay & 0x7fu; 50 | 51 | auto start = std::size_t{0}; 52 | 53 | if(a) 54 | start = std::size(decompressed) - y - 1; 55 | else 56 | start = (y * 0x100) + input.get(); 57 | 58 | if(start >= std::size(decompressed)) 59 | throw std::runtime_error{"Pkmn_gen2: start address out of range"}; 60 | 61 | switch(cmd) 62 | { 63 | case 0x4: // repeat 64 | for(auto i = start; i < start + length; ++i) 65 | decompressed.emplace_back(decompressed[i]); 66 | return; 67 | 68 | case 0x5: // bit-reverse repeat 69 | { 70 | auto reversebits = [](std::uint8_t b) 71 | { 72 | auto b2 = decltype(b){0}; 73 | for(auto i = 0; i < 8; ++i) 74 | { 75 | b2 <<= 1; 76 | b2 |= b & 0x01u; 77 | b >>= 1; 78 | } 79 | return b2; 80 | }; 81 | 82 | for(auto i = start; i < start + length; ++i) 83 | decompressed.emplace_back(reversebits(decompressed[i])); 84 | 85 | return; 86 | } 87 | 88 | case 0x6: // backwards repeat 89 | for(auto i = start + 1; i-- > start - length + 1;) 90 | { 91 | if(i >= std::size(decompressed)) 92 | throw std::runtime_error{"Pkmn_gen2: end address out of range"}; 93 | decompressed.emplace_back(decompressed[i]); 94 | } 95 | return; 96 | } 97 | return; 98 | } 99 | case 0x7: // long header 100 | { 101 | auto sub_cmd = static_cast(((length - 1) & 0x1cu) >> 2); 102 | auto sub_length = (((length - 1) & 0x3u) << 8 | input.get()) + 1; 103 | 104 | if(sub_cmd == 0x07u) 105 | throw std::runtime_error{"Pkmn_gen2 LZ3 sub-command is 0x07"}; 106 | 107 | process_cmd(input, decompressed, sub_cmd, sub_length); 108 | return; 109 | } 110 | 111 | default: 112 | throw std::logic_error{"Pkmn_gen2: Invalid cmd code"}; 113 | } 114 | } 115 | 116 | std::vector lz3_decompress(std::istream & input) 117 | { 118 | std::vector decompressed; 119 | 120 | while(true) 121 | { 122 | auto header = static_cast(input.get()); 123 | if(header == 0xff) 124 | return decompressed; 125 | 126 | auto cmd = (header & 0xe0) >> 5; 127 | auto length = (header & 0x1f) + 1; 128 | 129 | process_cmd(input, decompressed, cmd, length); 130 | } 131 | 132 | return decompressed; 133 | } 134 | 135 | void Pkmn_gen2::open(std::istream & input, const Args &) 136 | { 137 | input.exceptions(std::ios_base::badbit | std::ios_base::failbit); 138 | try 139 | { 140 | // So... This isn't actually a real format. 141 | // The gen2 pokemon games store the pokemon sprites as LZ3 compressed 2bpp tiles 142 | // To lay the tiles out correctly requires the size, which is stored seperately 143 | // the palette is also stored seperately. 144 | // For gen1, this wasn't a problem. a redundant copy of the size was stored alongdide the image data, 145 | // and the palette was set system-wide (grayscale for the original GB 146 | // and pocket, and one of a handful of presets on GBC/GBA. The super 147 | // gameboy was a bit of an exception, because the game stores a palette index for each mon) 148 | 149 | // to make this usable, we have 3 options (and we're going to support them all) 150 | // 1. guess the layout and use a grayscale palette. The games only actually use a handful of sprite sizes, and we can figure out which it is from the # of tiles. 151 | // 2. allow setting the size and/or palette from Sub_args 152 | // 3. an LZ3 stream ends with an 0xFF byte. If our input begins with 0xFF, assume the next byte is dimensions, and the next 12 bytes is a 4-entry RGB palette, and the LZ3 data follows 153 | 154 | // 3 overrides 1, and 2 overrides both 3 and 1 155 | 156 | auto tile_width = static_cast(tile_width_); 157 | auto tile_height = static_cast(tile_height_); 158 | 159 | auto detect_header = input.get(); 160 | if(detect_header == 0xff) 161 | { 162 | auto size = static_cast(input.get()); 163 | if(tile_width == 0 || tile_height == 0) 164 | { 165 | tile_width = size >> 4; 166 | tile_height = size & 0xf; 167 | } 168 | 169 | for(auto i = 0; i < 12; ++i) 170 | { 171 | if(palette_set_) 172 | input.get(); 173 | else 174 | palette_entries_[i / 3][i % 3] = input.get(); 175 | } 176 | palette_set_ = true; 177 | } 178 | else 179 | input.putback(detect_header); 180 | 181 | auto tiles = std::vector{}; 182 | if(uncompressed_) 183 | { 184 | input.exceptions(std::ios_base::badbit); // disable EOF exception 185 | tiles = read_input_to_memory(input); 186 | } 187 | else 188 | tiles = lz3_decompress(input); 189 | 190 | if(std::size(tiles) % tile_bytes != 0) 191 | throw std::runtime_error{"Pkmn_gen2 decompressed sprite data has odd size (" + std::to_string(std::size(tiles)) + " bytes)"}; 192 | 193 | if(tile_width == 0 || tile_height == 0) 194 | { 195 | auto num_tiles = std::size(tiles) / tile_bytes; 196 | const auto size_map = std::map> { 197 | {24, {6, 4}}, 198 | {25, {5, 5}}, 199 | {36, {6, 6}}, 200 | {49, {7, 7}} 201 | }; 202 | 203 | if(auto found = size_map.find(num_tiles); found != std::end(size_map)) 204 | { 205 | std::tie(tile_width, tile_height) = found->second; 206 | } 207 | else 208 | { 209 | tile_width = 1; 210 | tile_height = num_tiles; 211 | } 212 | } 213 | 214 | while(tile_width * tile_height > std::size(tiles) / tile_bytes) 215 | tiles.emplace_back(0); 216 | 217 | set_size(tile_dims * tile_width, tile_dims * tile_height); 218 | 219 | if(!palette_set_) 220 | palette_entries_ = Pkmn_gen1::palettes.at("greyscale"); 221 | 222 | for(auto i = 0u, tile_col = 0u, row = 0u; i < tile_width * tile_height * tile_bytes; i += 2u, ++row) 223 | { 224 | if(row == tile_height * tile_dims) 225 | { 226 | row = 0; 227 | ++tile_col; 228 | } 229 | 230 | for(auto col = 0u, b = 8u; b -- > 0; ++col) 231 | { 232 | auto bit0 = (tiles[i + 1] >> b) & 0x1; 233 | auto bit1 = (tiles[i + 0] >> b) & 0x1; 234 | image_data_[row][tile_col * tile_dims + col] = palette_entries_[bit1 << 1 | bit0]; 235 | } 236 | } 237 | } 238 | catch(std::ios_base::failure & e) 239 | { 240 | if(input.bad()) 241 | throw std::runtime_error{"Error reading Pkmn sprite: could not read file"}; 242 | else 243 | throw std::runtime_error{"Error reading Pkmn sprite: unexpected end of file"}; 244 | } 245 | } 246 | 247 | void Pkmn_gen2::handle_extra_args(const Args & args) 248 | { 249 | auto options = Sub_args{"Pokemon Gen 2 Sprite"}; 250 | try 251 | { 252 | auto palette_list = std::string{}; 253 | for(auto &&[s,_]: Pkmn_gen1::palettes) 254 | { 255 | if(!std::empty(palette_list)) 256 | palette_list += ", "; 257 | palette_list += s; 258 | } 259 | 260 | options.add_options() 261 | ("uncompressed", "Specify that the input is uncompressed 2bpp gameboy tiles (as used in a few Crystal version sprites)") 262 | ("tile-width", "Specify width for tile layout [1-15]", cxxopts::value(), "WIDTH") 263 | ("tile-height", "Specify height for tile layout [1-15]", cxxopts::value(), "HEIGHT") 264 | ("palette", "Palette to display or convert into. Valid values are: " + palette_list, cxxopts::value(), "PALETTE") 265 | ("palette-colors", "Specify a comma-seperated list of palette RGB values [0-255]. 4 colors (12 values) should be specified. If 2 colors (6 values) are entered, they are assumed to be the middle 2 color indexes, and the first is assumed to be white and the last to be black (as is the case for in-game palettes). Overrides --palette", 266 | cxxopts::value>(), "COLORS"); 267 | 268 | auto sub_args = options.parse(args.extra_args); 269 | 270 | uncompressed_ = sub_args.count("uncompressed"); 271 | 272 | if(( sub_args.count("tile-width") && !sub_args.count("tile-height")) || 273 | (!sub_args.count("tile-width") && sub_args.count("tile-height"))) 274 | { 275 | throw std::runtime_error{options.help(args.help_text) + "\nMust specify --tile-width and --tile-height together"}; 276 | } 277 | else if(sub_args.count("tile-width") && sub_args.count("tile-height")) 278 | { 279 | tile_width_ = sub_args["tile-width"].as(); 280 | tile_height_ = sub_args["tile-height"].as(); 281 | 282 | if(tile_width_ == 0 || tile_width_ > 15) 283 | throw std::runtime_error{options.help(args.help_text) + "\n--tile-width out of range [1-15]"}; 284 | if(tile_height_ == 0 || tile_height_ > 15) 285 | throw std::runtime_error{options.help(args.help_text) + "\n--tile-height out of range [1-15]"}; 286 | } 287 | 288 | if(sub_args.count("palette-colors")) 289 | { 290 | auto & colors = sub_args["palette-colors"].as>(); 291 | auto color_count = std::size(colors); 292 | 293 | if(color_count != 12 && color_count != 6) 294 | throw std::runtime_error{options.help(args.help_text) + "\nMust specify 12 or 6 RGB values for --palette-colors. " + std::to_string(color_count) + " specified"}; 295 | 296 | for(auto i = 0u; i < color_count; ++i) 297 | { 298 | auto c = colors[i]; 299 | if(c > 255) 300 | throw std::runtime_error{options.help(args.help_text) + "\n--palette-colors entry (" + std::to_string(c) + ") out of range [0-255]"}; 301 | 302 | palette_entries_[color_count == 12 ? i / 3 : i / 3 + 1][i % 3] = c; 303 | } 304 | 305 | if(color_count == 6) 306 | { 307 | palette_entries_[0] = Color{0xff}; 308 | palette_entries_[3] = Color{0x00}; 309 | } 310 | palette_set_ = true; 311 | } 312 | else if(sub_args.count("palette")) 313 | { 314 | auto palette = sub_args["palette"].as(); 315 | if(!Pkmn_gen1::palettes.contains(palette)) 316 | throw std::runtime_error{options.help(args.help_text) + "\n'" + palette + "' is not valid for --palette. Valid values are: " + palette_list}; 317 | 318 | palette_entries_ = Pkmn_gen1::palettes.at(palette); 319 | palette_set_ = true; 320 | } 321 | } 322 | catch(const cxxopt_exception & e) 323 | { 324 | throw std::runtime_error{options.help(args.help_text) + '\n' + e.what()}; 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /codecs/pkmn_gen2.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PKMN_GEN2_HPP 2 | #define PKMN_GEN2_HPP 3 | 4 | #include "image.hpp" 5 | 6 | class Pkmn_gen2 final: public Image 7 | { 8 | public: 9 | Pkmn_gen2() = default; 10 | void open(std::istream & input, const Args & args) override; 11 | 12 | void handle_extra_args(const Args & args) override; 13 | 14 | private: 15 | unsigned int tile_width_ {0}, tile_height_ {0}; 16 | 17 | bool uncompressed_ {false}; 18 | bool palette_set_ {false}; 19 | std::array palette_entries_; 20 | }; 21 | #endif // PKMN_GEN2_HPP 22 | -------------------------------------------------------------------------------- /codecs/png.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PNG_HPP 2 | #define PNG_HPP 3 | 4 | #include "image.hpp" 5 | 6 | inline bool is_png(const Image::Header & header) 7 | { 8 | const std::array png_header = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; 9 | return std::equal(std::begin(png_header), std::end(png_header), std::begin(header), Image::header_cmp); 10 | } 11 | 12 | #ifdef PNG_FOUND 13 | class Png final: public Image 14 | { 15 | public: 16 | Png() = default; 17 | void open(std::istream & input, const Args & args) override; 18 | 19 | void handle_extra_args(const Args & args) override; 20 | bool supports_multiple_images() const override { return supports_multiple_images_; } 21 | bool supports_animation() const override { return supports_animation_; } 22 | 23 | static void write(std::ostream & out, const Image & img, bool invert); 24 | 25 | private: 26 | bool composed_ {true}; 27 | bool supports_multiple_images_ {true}; 28 | bool supports_animation_ {true}; 29 | }; 30 | #endif 31 | #endif // PNG_HPP 32 | -------------------------------------------------------------------------------- /codecs/pnm.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PNM_HPP 2 | #define PNM_HPP 3 | 4 | #include "image.hpp" 5 | 6 | inline bool is_pnm(const Image::Header & header) 7 | { 8 | const std::array pbm_header {'P', '1'}; 9 | const std::array pgm_header {'P', '2'}; 10 | const std::array ppm_header {'P', '3'}; 11 | const std::array pbm_b_header {'P', '4'}; 12 | const std::array pgm_b_header {'P', '5'}; 13 | const std::array ppm_b_header {'P', '6'}; 14 | const std::array pam_header {'P', '7'}; 15 | const std::array pfm_c_header {'P', 'F'}; 16 | const std::array pfm_g_header {'P', 'f'}; 17 | 18 | return std::equal(std::begin(pbm_header), std::end(pbm_header), std::begin(header), Image::header_cmp) 19 | || std::equal(std::begin(pgm_header), std::end(pgm_header), std::begin(header), Image::header_cmp) 20 | || std::equal(std::begin(ppm_header), std::end(ppm_header), std::begin(header), Image::header_cmp) 21 | || std::equal(std::begin(pbm_b_header), std::end(pbm_b_header), std::begin(header), Image::header_cmp) 22 | || std::equal(std::begin(pgm_b_header), std::end(pgm_b_header), std::begin(header), Image::header_cmp) 23 | || std::equal(std::begin(ppm_b_header), std::end(ppm_b_header), std::begin(header), Image::header_cmp) 24 | || std::equal(std::begin(pam_header), std::end(pam_header), std::begin(header), Image::header_cmp) 25 | || std::equal(std::begin(pfm_c_header), std::end(pfm_c_header), std::begin(header), Image::header_cmp) 26 | || std::equal(std::begin(pfm_g_header), std::end(pfm_g_header), std::begin(header), Image::header_cmp); 27 | } 28 | 29 | class Pnm final: public Image 30 | { 31 | public: 32 | Pnm() = default; 33 | void open(std::istream & input, const Args & args) override; 34 | 35 | static void write_pbm(std::ostream & path, const Image & image, unsigned char bg, bool invert); 36 | static void write_pgm(std::ostream & path, const Image & image, unsigned char bg, bool invert); 37 | static void write_ppm(std::ostream & path, const Image & image, unsigned char bg, bool invert); 38 | static void write_pam(std::ostream & path, const Image & image, bool invert); 39 | static void write_pfm(std::ostream & path, const Image & image, unsigned char bg, bool invert); 40 | 41 | private: 42 | void read_P1(std::istream & input); 43 | void read_P2(std::istream & input); 44 | void read_P3(std::istream & input); 45 | void read_P4(std::istream & input); 46 | void read_P5(std::istream & input); 47 | void read_P6(std::istream & input); 48 | void read_P7(std::istream & input); 49 | void read_PF_color(std::istream & input); 50 | void read_PF_gray(std::istream & input); 51 | }; 52 | #endif // PNM_HPP 53 | -------------------------------------------------------------------------------- /codecs/sif.cpp: -------------------------------------------------------------------------------- 1 | #include "sif.hpp" 2 | 3 | #include 4 | 5 | void Sif::open(std::istream & input, const Args &) 6 | { 7 | input.exceptions(std::ios_base::badbit | std::ios_base::failbit); 8 | try 9 | { 10 | set_size(25, 6); 11 | for(auto && row: image_data_) 12 | std::fill(std::begin(row), std::end(row), Color{0, 0, 0, 0}); 13 | 14 | std::size_t y = 0, x = 0; 15 | for(auto i = std::istreambuf_iterator{input}; i != std::istreambuf_iterator{}; ++i) 16 | { 17 | switch(*i) 18 | { 19 | case '0': 20 | if(image_data_[y][x].a == 0) 21 | image_data_[y][x] = {0, 0, 0, 255}; 22 | break; 23 | case '1': 24 | if(image_data_[y][x].a == 0) 25 | image_data_[y][x] = {255, 255, 255, 255}; 26 | break; 27 | case '2': 28 | break; 29 | default: 30 | continue; 31 | } 32 | 33 | if(++x == width_) 34 | { 35 | x = 0; 36 | if(++y == height_) 37 | { 38 | y = 0; 39 | } 40 | } 41 | } 42 | } 43 | catch(std::ios_base::failure & e) 44 | { 45 | if(input.bad()) 46 | throw std::runtime_error{"Error reading SIF: could not read file"}; 47 | else 48 | throw std::runtime_error{"Error reading SIF: unexpected end of file"}; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /codecs/sif.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SIF_HPP 2 | #define SIF_HPP 3 | 4 | #include "image.hpp" 5 | 6 | class Sif final: public Image 7 | { 8 | public: 9 | Sif() = default; 10 | void open(std::istream & input, const Args & args) override; 11 | }; 12 | #endif // SIF_HPP 13 | -------------------------------------------------------------------------------- /codecs/srf.cpp: -------------------------------------------------------------------------------- 1 | #include "srf.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include "binio.hpp" 12 | #include "sub_args.hpp" 13 | 14 | // Garmin GPS Vehicle icon 15 | // Format documented at http://www.techmods.net/nuvi/ 16 | 17 | struct SRF_image 18 | { 19 | std::uint16_t height {0}; 20 | std::uint16_t width {0}; 21 | std::vector alpha_mask; 22 | std::vector image; 23 | }; 24 | 25 | SRF_image read_image_data(std::istream & input) 26 | { 27 | // header 28 | input.ignore(12); // unknown 29 | 30 | SRF_image im; 31 | 32 | readb(input, im.height); 33 | readb(input, im.width); 34 | 35 | input.ignore(2); // unknown 36 | 37 | std::uint16_t rowstride; 38 | readb(input, rowstride); 39 | if(rowstride != im.width * 2) 40 | throw std::runtime_error{"SRF rowstride mismatched " + std::to_string(rowstride) + " vs " + std::to_string(im.width * 2)}; 41 | 42 | input.ignore(4); // unknown 43 | 44 | // Alpha Mask 45 | input.ignore(4); // unknown 46 | 47 | std::uint32_t alpha_mask_size; 48 | readb(input, alpha_mask_size); 49 | 50 | if(alpha_mask_size != im.width * im.height) 51 | throw std::runtime_error{"SRF alpha size mismatched " + std::to_string(alpha_mask_size) + " vs " + std::to_string(im.width * im.height)}; 52 | 53 | im.alpha_mask.resize(im.width * im.height); 54 | input.read(reinterpret_cast(std::data(im.alpha_mask)), std::size(im.alpha_mask) * sizeof(decltype(im.alpha_mask)::value_type)); 55 | 56 | // image data (16-bit) 57 | input.ignore(4); // unknown 58 | 59 | std::uint32_t image_size; 60 | readb(input, image_size); 61 | if(image_size != im.width * im.height * 2) 62 | throw std::runtime_error{"SRF image size mismatched " + std::to_string(image_size) + " vs " + std::to_string(im.width * im.height * 2)}; 63 | 64 | im.image.resize(im.width * im.height); 65 | if(image_size) 66 | input.read(reinterpret_cast(std::data(im.image)), std::size(im.image) * sizeof(decltype(im.image)::value_type)); 67 | 68 | return im; 69 | } 70 | 71 | Color get_image_color(std::size_t row, 72 | std::size_t col, 73 | const SRF_image & im) 74 | { 75 | auto alpha = static_cast(std::lround((static_cast(128 - im.alpha_mask[row * im.width + col])) * 255.0f / 128.0f)); // 0-128, awkward range 76 | 77 | auto raw_color = im.image[row * im.width + col]; 78 | auto r = static_cast((raw_color & 0xF800) >> 11) << 3; 79 | auto g = static_cast((raw_color & 0x07C0) >> 6) << 3; 80 | auto b = static_cast((raw_color & 0x003F) >> 0) << 3; 81 | return Color(r, g, b, alpha); 82 | } 83 | 84 | constexpr auto frames_per_image = 36u; 85 | void Srf::open(std::istream & input, const Args & args) 86 | { 87 | input.exceptions(std::ios_base::badbit | std::ios_base::failbit); 88 | try 89 | { 90 | // header 91 | input.ignore(16); // magic 92 | input.ignore(8); // unknown 93 | 94 | auto num_images = readb(input); 95 | 96 | input.ignore(4); 97 | std::uint32_t garmin_strlen; 98 | readb(input, garmin_strlen); 99 | input.ignore(garmin_strlen); // 578 string 100 | 101 | std::uint32_t unknown_switch; 102 | readb(input, unknown_switch); 103 | if(unknown_switch != 6) 104 | throw std::runtime_error{"Unsupported SRF image format"}; 105 | 106 | readb(input, garmin_strlen); 107 | input.ignore(garmin_strlen); // version no 108 | input.ignore(4); // unknown 109 | readb(input, garmin_strlen); 110 | input.ignore(garmin_strlen); // product code? 111 | 112 | std::vector image_sets; 113 | 114 | std::uint16_t max_width = 0; 115 | std::uint16_t total_height = 0; 116 | 117 | for(std::uint32_t i = 0u; i < num_images; ++i) 118 | { 119 | image_sets.emplace_back(read_image_data(input)); 120 | max_width = std::max(max_width, image_sets.back().width); 121 | total_height += image_sets.back().height; 122 | } 123 | 124 | if(args.frame_no) 125 | { 126 | if(!args.image_no) 127 | throw std::runtime_error{"Error processing SRF: --frame-no set without --image-no"}; 128 | 129 | this_is_first_image_ = true; 130 | supports_animation_ = false; 131 | 132 | const auto frame_width = image_sets[*args.image_no].width / frames_per_image; 133 | 134 | set_size(frame_width, image_sets[*args.image_no].height); 135 | 136 | for(std::size_t row = 0u; row < image_sets[*args.image_no].height; ++row) 137 | { 138 | for(std::size_t col = *args.frame_no * frame_width, current_col = 0u; col < (*args.frame_no + 1) * frame_width; ++col, ++current_col) 139 | image_data_[row][current_col] = get_image_color(row, col, image_sets[*args.image_no]); 140 | } 141 | } 142 | else if(mosaic_) 143 | { 144 | this_is_first_image_ = true; 145 | supports_animation_ = false; 146 | 147 | if(args.image_no) 148 | { 149 | set_size(image_sets[*args.image_no].width, image_sets[*args.image_no].height); 150 | 151 | for(std::size_t row = 0u; row < image_sets[*args.image_no].height; ++row) 152 | { 153 | for(std::size_t col = 0u; col < image_sets[*args.image_no].width; ++col) 154 | image_data_[row][col] = get_image_color(row, col, image_sets[*args.image_no]); 155 | } 156 | } 157 | else 158 | { 159 | set_size(max_width, total_height); 160 | 161 | std::size_t current_row = 0; 162 | for(std::uint32_t i = 0u; i < num_images; ++i) 163 | { 164 | for(std::size_t row = 0u; row < image_sets[i].height; ++row, ++current_row) 165 | { 166 | for(std::size_t col = 0u; col < image_sets[i].width; ++col) 167 | image_data_[current_row][col] = get_image_color(row, col, image_sets[i]); 168 | } 169 | } 170 | } 171 | } 172 | else 173 | { 174 | this_is_first_image_ = false; 175 | supports_animation_ = true; 176 | 177 | if(args.image_no) 178 | { 179 | const auto frame_width = image_sets[*args.image_no].width / frames_per_image; 180 | 181 | for(std::uint32_t i = 0u; i < frames_per_image; ++i) 182 | { 183 | auto & frame = images_.emplace_back(frame_width, image_sets[*args.image_no].height); 184 | for(std::size_t row = 0u; row < image_sets[*args.image_no].height; ++row) 185 | { 186 | for(std::size_t col = i * frame_width, current_col = 0u; col < (i + 1) * frame_width; ++col, ++current_col) 187 | frame[row][current_col] = get_image_color(row, col, image_sets[*args.image_no]); 188 | } 189 | } 190 | } 191 | else 192 | { 193 | for(std::uint32_t i = 0u; i < num_images; ++i) 194 | { 195 | auto & img = images_.emplace_back(image_sets[i].width, image_sets[i].height); 196 | for(std::size_t row = 0u; row < image_sets[i].height; ++row) 197 | { 198 | for(std::size_t col = 0u; col < image_sets[i].width; ++col) 199 | img[row][col] = get_image_color(row, col, image_sets[i]); 200 | } 201 | } 202 | } 203 | } 204 | } 205 | catch(std::ios_base::failure & e) 206 | { 207 | if(input.bad()) 208 | throw std::runtime_error{"Error reading SRF: could not read file"}; 209 | else 210 | throw std::runtime_error{"Error reading SRF: unexpected end of file"}; 211 | } 212 | } 213 | 214 | void Srf::handle_extra_args(const Args & args) 215 | { 216 | auto options = Sub_args{"SRF"}; 217 | try 218 | { 219 | options.add_options() 220 | ("mosaic", "Without --image-no, shows all images as a mosic. With --image-no, shows a mosaic of each frame in the selected image. Invalid with --frame-no"); 221 | 222 | auto sub_args = options.parse(args.extra_args); 223 | 224 | mosaic_ = sub_args.count("mosaic"); 225 | if(args.frame_no && mosaic_) 226 | throw std::runtime_error{options.help(args.help_text) + "\nCan't specify --mosaic with --frame-no"}; 227 | } 228 | catch(const cxxopt_exception & e) 229 | { 230 | throw std::runtime_error{options.help(args.help_text) + '\n' + e.what()}; 231 | } 232 | } 233 | 234 | std::chrono::duration Srf::get_frame_delay(std::size_t) const 235 | { 236 | return std::chrono::duration(1.0f / 25.0f); 237 | } 238 | -------------------------------------------------------------------------------- /codecs/srf.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SRF_HPP 2 | #define SRF_HPP 3 | 4 | #include "image.hpp" 5 | 6 | inline bool is_srf(const Image::Header & header) 7 | { 8 | const std::array srf_header = {'G', 'A', 'R', 'M', 'I', 'N', ' ', 'B', 'I', 'T', 'M', 'A'}; // technically, the header is 16 bytes, but this is probably enough. 9 | 10 | return std::equal(std::begin(srf_header), std::end(srf_header), std::begin(header), Image::header_cmp); 11 | } 12 | 13 | class Srf final: public Image 14 | { 15 | public: 16 | Srf() = default; 17 | void open(std::istream & input, const Args & args) override; 18 | 19 | void handle_extra_args(const Args & args) override; 20 | bool supports_animation() const override { return supports_animation_; } 21 | bool supports_multiple_images() const override { return supports_animation_; } 22 | bool supports_subimages() const override { return true; } 23 | 24 | std::chrono::duration get_frame_delay(std::size_t) const override; 25 | 26 | private: 27 | bool supports_animation_ {true}; 28 | bool mosaic_ {false}; 29 | }; 30 | #endif // SRF_HPP 31 | -------------------------------------------------------------------------------- /codecs/sub_args.cpp: -------------------------------------------------------------------------------- 1 | #include "sub_args.hpp" 2 | 3 | Sub_args::Sub_args(const std::string & group_name): 4 | Options{""}, 5 | group_name_{group_name} 6 | {} 7 | 8 | cxxopts::OptionAdder Sub_args::add_options() 9 | { 10 | return cxxopts::Options::add_options(group_name_); 11 | } 12 | 13 | cxxopts::ParseResult Sub_args::parse(const std::vector & args) 14 | { 15 | #if CXXOPTS__VERSION_MAJOR >= 3 16 | auto argv = std::vector{}; 17 | argv.emplace_back(""); 18 | for(auto && i: args) 19 | argv.emplace_back(i.c_str()); 20 | 21 | return cxxopts::Options::parse(std::size(argv), std::data(argv)); 22 | #else 23 | auto args_copy = args; 24 | auto argv = std::vector{}; 25 | std::string arg0 = ""; 26 | argv.emplace_back(arg0.data()); 27 | for(auto && i: args_copy) 28 | argv.emplace_back(i.data()); 29 | 30 | int argc = std::size(argv); 31 | auto argv_p = std::data(argv); 32 | return cxxopts::Options::parse(argc, argv_p); 33 | #endif 34 | } 35 | 36 | std::string Sub_args::help(const std::string & main_help) const 37 | { 38 | auto sub_help = cxxopts::Options::help(); 39 | constexpr auto newline_count = 4u; 40 | auto count = 0u, pos = 0u; 41 | for(; pos < std::size(sub_help) && count < newline_count; ++pos) 42 | { 43 | if(sub_help[pos] == '\n') 44 | ++count; 45 | } 46 | return main_help + "\n" + sub_help.substr(pos); 47 | } 48 | -------------------------------------------------------------------------------- /codecs/sub_args.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SUB_ARGS_HPP 2 | #define SUB_ARGS_HPP 3 | 4 | #include 5 | #include 6 | 7 | #include "../cxxopts_wrapper.hpp" 8 | 9 | class Sub_args: public cxxopts::Options 10 | { 11 | public: 12 | Sub_args(const std::string & group_name); 13 | cxxopts::OptionAdder add_options(); 14 | cxxopts::ParseResult parse(const std::vector & args); 15 | std::string help(const std::string & main_help) const; 16 | private: 17 | std::string group_name_; 18 | }; 19 | 20 | #endif // SUB_ARGS_HPP 21 | -------------------------------------------------------------------------------- /codecs/svg.cpp: -------------------------------------------------------------------------------- 1 | #include "svg.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | struct RAII_stack 9 | { 10 | ~RAII_stack() 11 | { 12 | for(auto i = std::rbegin(objs); i != std::rend(objs); ++i) 13 | { 14 | auto [d, free_fun] = *i; 15 | free_fun(d); 16 | } 17 | } 18 | template 19 | void push(T * d, void (*free_fun)(U*)) 20 | { 21 | objs.emplace_back(reinterpret_cast(d), reinterpret_cast(free_fun)); 22 | } 23 | 24 | std::vector> objs; 25 | }; 26 | 27 | RsvgHandle * get_svg_handle(std::istream & input, const std::string & filename) 28 | { 29 | auto data = Image::read_input_to_memory(input); 30 | 31 | GFile * base = g_file_new_for_path((filename == "-") ? "." : filename.c_str()); 32 | GInputStream * is = g_memory_input_stream_new_from_data(std::data(data), std::size(data), nullptr); 33 | 34 | GError * err {nullptr}; 35 | RsvgHandle * svg_handle = rsvg_handle_new_from_stream_sync(is, base, RSVG_HANDLE_FLAGS_NONE, nullptr, &err); 36 | 37 | g_object_unref(is); 38 | g_object_unref(base); 39 | data.clear(); 40 | 41 | if(!svg_handle) 42 | { 43 | std::string message {err->message}; 44 | g_error_free(err); 45 | throw std::runtime_error { "Error reading SVG: " + message }; 46 | } 47 | 48 | return svg_handle; 49 | } 50 | 51 | void Svg::open(std::istream & input, const Args & args) 52 | { 53 | RAII_stack rs; 54 | 55 | RsvgHandle * svg_handle = get_svg_handle(input, args.input_filename); 56 | rs.push(svg_handle, g_object_unref); 57 | 58 | rsvg_handle_set_dpi(svg_handle, 75.0); 59 | gdouble width{0.0}, height{0.0}; 60 | #if LIBRSVG_MAJOR_VERSION > 2 || (LIBRSVG_MAJOR_VERSION == 2 && LIBRSVG_MINOR_VERSION >= 52) 61 | rsvg_handle_get_intrinsic_size_in_pixels(svg_handle, &width, &height); 62 | 63 | #else 64 | RsvgDimensionData dims; 65 | rsvg_handle_get_dimensions(svg_handle, &dims); 66 | width = dims.width; 67 | height = dims.height; 68 | #endif 69 | 70 | cairo_surface_t * bmp = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); 71 | rs.push(bmp, cairo_surface_destroy); 72 | if(cairo_surface_status(bmp) != CAIRO_STATUS_SUCCESS) 73 | throw std::runtime_error { "Error creating SVG cairo surface" }; 74 | 75 | cairo_t * cr = cairo_create(bmp); 76 | rs.push(cr, cairo_destroy); 77 | if(cairo_status(cr) != CAIRO_STATUS_SUCCESS) 78 | throw std::runtime_error {"Error creating SVG cairo object"}; 79 | 80 | #if LIBRSVG_MAJOR_VERSION > 2 || (LIBRSVG_MAJOR_VERSION == 2 && LIBRSVG_MINOR_VERSION >= 52) 81 | auto viewport = RsvgRectangle {.x=0.0, .y=0.0, .width=width, .height=height}; 82 | if(GError * err = nullptr; !rsvg_handle_render_document(svg_handle, cr, &viewport, &err)) 83 | { 84 | std::string message {err->message}; 85 | g_error_free(err); 86 | throw std::runtime_error{"Error rendering SVG: " + message}; 87 | } 88 | #else 89 | if(!rsvg_handle_render_cairo(svg_handle, cr)) 90 | { 91 | throw std::runtime_error{"Error rendering SVG"}; 92 | } 93 | #endif 94 | 95 | set_size(width, height); 96 | if(static_cast(cairo_image_surface_get_stride(bmp)) < width_ * 4) 97 | throw std::runtime_error {"Invalid SVG stride"}; 98 | 99 | for(std::size_t row = 0; row < height_; ++row) 100 | { 101 | for(std::size_t col = 0; col < width_; ++col) 102 | { 103 | std::size_t pix_i = cairo_image_surface_get_stride(bmp) * row + 4 * col; 104 | 105 | image_data_[row][col].b = cairo_image_surface_get_data(bmp)[pix_i]; 106 | image_data_[row][col].g = cairo_image_surface_get_data(bmp)[pix_i + 1]; 107 | image_data_[row][col].r = cairo_image_surface_get_data(bmp)[pix_i + 2]; 108 | image_data_[row][col].a = cairo_image_surface_get_data(bmp)[pix_i + 3]; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /codecs/svg.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SVG_HPP 2 | #define SVG_HPP 3 | 4 | #include "image.hpp" 5 | 6 | #ifdef SVG_FOUND 7 | class Svg final: public Image 8 | { 9 | public: 10 | Svg() = default; 11 | void open(std::istream & input, const Args & args) override; 12 | }; 13 | #endif 14 | #endif // SVG_HPP 15 | -------------------------------------------------------------------------------- /codecs/tga.cpp: -------------------------------------------------------------------------------- 1 | #include "tga.hpp" 2 | 3 | #include 4 | 5 | #include "binio.hpp" 6 | 7 | struct Tga_data 8 | { 9 | bool rle_compressed {false}; 10 | enum class Color_type: std::uint8_t { indexed=1, color=2, grayscale=3 } color; 11 | std::uint16_t width {0}; 12 | std::uint16_t height {0}; 13 | std::uint8_t bpp {0}; 14 | bool bottom_to_top { true}; 15 | 16 | std::vector palette; 17 | }; 18 | 19 | Color read_pixel(const unsigned char * byte, Tga_data::Color_type color, std::uint8_t bpp, const std::vector & palette) 20 | { 21 | if(color == Tga_data::Color_type::color) 22 | { 23 | Color c; 24 | 25 | switch(bpp) 26 | { 27 | case 15: 28 | case 16: 29 | // arrrrrgg gggbbbbb - He must have died while carving it. 30 | if(bpp == 16) 31 | c.a = ((byte[1] >> 7) & 0x01) * 255; 32 | 33 | c.r = (byte[1] & 0x7C) << 1; 34 | c.g = ((byte[1] & 0x03) << 6) | ((byte[0] & 0xE0) >> 2); 35 | c.b = (byte[0] & 0x1F) << 3; 36 | 37 | break; 38 | case 24: 39 | c.b = byte[0]; 40 | c.g = byte[1]; 41 | c.r = byte[2]; 42 | break; 43 | case 32: 44 | c.b = byte[0]; 45 | c.g = byte[1]; 46 | c.r = byte[2]; 47 | c.a = byte[3]; 48 | break; 49 | } 50 | 51 | return c; 52 | } 53 | else if(color == Tga_data::Color_type::indexed) 54 | { 55 | switch(bpp) 56 | { 57 | case 8: 58 | return palette[byte[0]]; 59 | break; 60 | case 15: 61 | case 16: 62 | { 63 | std::uint16_t idx = static_cast(byte[1]) << 8 | static_cast(byte[0]); 64 | return palette[idx]; 65 | } 66 | } 67 | } 68 | else if(color == Tga_data::Color_type::grayscale) 69 | { 70 | return Color{byte[0]}; 71 | } 72 | 73 | return {}; 74 | } 75 | 76 | Tga_data read_tga_header(std::istream & in) 77 | { 78 | Tga_data tga; 79 | 80 | std::uint8_t id_length; 81 | readb(in, id_length); 82 | 83 | std::uint8_t color_map_type; 84 | readb(in, color_map_type); 85 | if(color_map_type > 1) 86 | throw std::runtime_error {"Unsupported TGA color map type: " + std::to_string((int)color_map_type)}; 87 | 88 | std::uint8_t image_type = 0; 89 | readb(in, image_type); 90 | if( image_type != 1 && image_type != 2 && image_type != 3 91 | && image_type != 9 && image_type != 10 && image_type != 11) 92 | { 93 | throw std::runtime_error {"Unsupported TGA image type: " + std::to_string((int)image_type)}; 94 | } 95 | 96 | tga.rle_compressed = image_type & 0x8; 97 | tga.color = static_cast(image_type & 0x3); 98 | 99 | std::uint16_t color_map_start_idx, color_map_num_entries; 100 | std::uint8_t color_map_bpp; 101 | readb(in, color_map_start_idx); 102 | readb(in, color_map_num_entries); 103 | readb(in, color_map_bpp); 104 | 105 | if(color_map_bpp != 0 && color_map_bpp != 8 && color_map_bpp != 15 && color_map_bpp != 16 && color_map_bpp != 24 && color_map_bpp != 32) 106 | throw std::runtime_error{"Unsupported TGA palette color depth: " + std::to_string((int)color_map_bpp)}; 107 | 108 | in.ignore(4); // skip origin 109 | readb(in, tga.width); 110 | readb(in, tga.height); 111 | readb(in, tga.bpp); 112 | 113 | std::uint8_t image_descriptor; 114 | readb(in, image_descriptor); 115 | tga.bottom_to_top = !((image_descriptor & 0x20) >> 5); // if 0, image is upside down 116 | auto interleaved = (image_descriptor & 0xC0) >> 6; 117 | 118 | if(interleaved) 119 | throw std::runtime_error{"TGA interleaving not supported"}; 120 | 121 | if(tga.bpp != 8 && tga.bpp != 15 && tga.bpp != 16 && tga.bpp != 24 && tga.bpp != 32) 122 | throw std::runtime_error{"Unsupported TGA color depth: " + std::to_string((int)tga.bpp)}; 123 | 124 | if(tga.color == Tga_data::Color_type::indexed && (tga.bpp == 24 || tga.bpp == 32)) 125 | throw std::runtime_error{"Unsupported TGA color depth in indexed mode: " + std::to_string((int)tga.bpp)}; 126 | 127 | if(tga.color == Tga_data::Color_type::color && tga.bpp == 8) 128 | throw std::runtime_error{"Unsupported TGA color depth in true-color mode: " + std::to_string((int)tga.bpp)}; 129 | 130 | if(tga.color == Tga_data::Color_type::grayscale && tga.bpp != 8) 131 | throw std::runtime_error{"Unsupported TGA color depth in grayscale mode: " + std::to_string((int)tga.bpp)}; 132 | 133 | // skip image ID block 134 | in.ignore(id_length); 135 | 136 | if(color_map_type) 137 | { 138 | if(tga.color == Tga_data::Color_type::indexed) 139 | { 140 | const std::uint8_t num_bytes = (color_map_bpp + 7) / 8; // ceiling division 141 | std::vector bytes(num_bytes); 142 | 143 | tga.palette.resize(color_map_num_entries); 144 | for(std::size_t i = color_map_start_idx; i < color_map_num_entries; ++i) 145 | { 146 | in.read(std::data(bytes), num_bytes); 147 | tga.palette[i] = read_pixel(reinterpret_cast(std::data(bytes)), Tga_data::Color_type::color, color_map_bpp, {}); 148 | } 149 | } 150 | else // skip the palette 151 | { 152 | in.ignore((color_map_num_entries - color_map_start_idx) * color_map_bpp / 8); 153 | } 154 | } 155 | else if(tga.color == Tga_data::Color_type::indexed) 156 | { 157 | throw std::runtime_error {"No palette defined for indexed TGA"}; 158 | } 159 | 160 | return tga; 161 | } 162 | 163 | void read_uncompressed(std::istream & in, const Tga_data & tga, std::vector> & image_data) 164 | { 165 | std::vector rowbuf(tga.width * tga.bpp / 8); 166 | for(std::size_t row = 0; row < tga.height; ++row) 167 | { 168 | auto im_row = tga.bottom_to_top ? tga.height - row - 1 : row; 169 | in.read(std::data(rowbuf), std::size(rowbuf)); 170 | for(std::size_t col = 0; col < tga.width; ++col) 171 | { 172 | image_data[im_row][col] = read_pixel(reinterpret_cast(std::data(rowbuf)) + (col *((tga.bpp + 7) / 8)), tga.color, tga.bpp, tga.palette); // ceiling division 173 | } 174 | } 175 | } 176 | 177 | void read_compressed(std::istream & in, const Tga_data & tga, std::vector> & image_data) 178 | { 179 | std::size_t row{0}; 180 | auto store_val = [&row, col = std::size_t{0}, im_row = (tga.bottom_to_top ? tga.height - row - 1 : row), &tga, &image_data](const Color & color) mutable 181 | { 182 | if(row == tga.height) 183 | throw std::runtime_error{"TGA data out of range"}; 184 | 185 | image_data[im_row][col] = color; 186 | 187 | if(++col == tga.width) 188 | { 189 | col = 0; 190 | ++row; 191 | im_row = tga.bottom_to_top ? tga.height - row - 1 : row; 192 | } 193 | }; 194 | 195 | const std::uint8_t num_bytes = (tga.bpp + 7) / 8; // ceiling division 196 | std::vector bytes(num_bytes); 197 | 198 | while(row < tga.height) 199 | { 200 | std::uint8_t b; 201 | readb(in, b); 202 | auto len = (b & 0x7F) + 1; 203 | 204 | if(b & 0x80) // rle packet 205 | { 206 | in.read(std::data(bytes), num_bytes); 207 | auto val = read_pixel(reinterpret_cast(std::data(bytes)), tga.color, tga.bpp, tga.palette); 208 | 209 | for(std::uint8_t i = 0; i < len; ++i) 210 | store_val(val); 211 | } 212 | else // raw packet 213 | { 214 | for(std::uint8_t i = 0; i < len; ++i) 215 | { 216 | in.read(std::data(bytes), num_bytes); 217 | auto val = read_pixel(reinterpret_cast(std::data(bytes)), tga.color, tga.bpp, tga.palette); 218 | store_val(val); 219 | } 220 | } 221 | } 222 | } 223 | 224 | void Tga::open(std::istream & input, const Args &) 225 | { 226 | input.exceptions(std::ios_base::badbit | std::ios_base::failbit); 227 | try 228 | { 229 | auto tga = read_tga_header(input); 230 | 231 | set_size(tga.width, tga.height); 232 | 233 | if(tga.rle_compressed) 234 | read_compressed(input, tga, image_data_); 235 | else 236 | read_uncompressed(input, tga, image_data_); 237 | 238 | } 239 | catch(std::ios_base::failure & e) 240 | { 241 | if(input.bad()) 242 | throw std::runtime_error{"Error reading TGA: could not read file"}; 243 | else 244 | throw std::runtime_error{"Error reading TGA: unexpected end of file"}; 245 | } 246 | } 247 | 248 | void Tga::write(std::ostream & out, const Image & img, bool invert) 249 | { 250 | if(img.get_width() > std::numeric_limits::max() || img.get_height() > std::numeric_limits::max()) 251 | throw std::runtime_error{"Image dimensions (" + std::to_string(img.get_width()) + "x" + std::to_string(img.get_height()) + ") exceed max TGA size (" + std::to_string(std::numeric_limits::max()) + "x" + std::to_string(std::numeric_limits::max()) + ")"}; 252 | 253 | writeb(out, std::uint8_t{0}); // image ID field size (omitted) 254 | writeb(out, std::uint8_t{0}); // color map type. 0 is none 255 | writeb(out, std::uint8_t{10}); // image type RLE compressed color 256 | 257 | // color map specification (unused, so all 0s) 258 | const std::array color_map_spec = {0, 0, 0, 0, 0}; 259 | out.write(reinterpret_cast(std::data(color_map_spec)), std::size(color_map_spec)); 260 | 261 | writeb(out, std::uint16_t{0}); // x origin 262 | writeb(out, std::uint16_t{0}); // y origin 263 | writeb(out, static_cast(img.get_width())); // width 264 | writeb(out, static_cast(img.get_height())); // height 265 | writeb(out, std::uint8_t{32}); // bpp 266 | writeb(out, std::uint8_t{0}); // image descriptor (store bottom to top, non-interleaved) 267 | 268 | for(std::size_t row = img.get_height(); row -- > 0;) 269 | { 270 | std::vector non_rle_buf; 271 | auto write_non_rle = [&non_rle_buf, &out]() 272 | { 273 | if(std::empty(non_rle_buf)) 274 | return; 275 | 276 | writeb(out, static_cast(std::size(non_rle_buf) - 1)); 277 | 278 | for(auto && color: non_rle_buf) 279 | { 280 | writeb(out, color.b); 281 | writeb(out, color.g); 282 | writeb(out, color.r); 283 | writeb(out, color.a); 284 | } 285 | non_rle_buf.clear(); 286 | }; 287 | 288 | auto write_rle = [&out](const Color & color, unsigned int count) 289 | { 290 | writeb(out, static_cast((count - 1) | 0x80u)); 291 | writeb(out, color.b); 292 | writeb(out, color.g); 293 | writeb(out, color.r); 294 | writeb(out, color.a); 295 | }; 296 | 297 | for(std::size_t col = 0; col < img.get_width();) 298 | { 299 | auto c = img[row][col]; 300 | 301 | unsigned int rle_count {1}; 302 | 303 | for(std::size_t x = col + 1; x < img.get_width() && img[row][x] == c && rle_count < 128; ++x, ++rle_count); // count number of consecutive pixels with the same color 304 | 305 | if(invert) 306 | { 307 | c.r = 255 - c.r; 308 | c.g = 255 - c.g; 309 | c.b = 255 - c.b; 310 | } 311 | 312 | if(rle_count > 2) 313 | { 314 | write_non_rle(); 315 | write_rle(c, rle_count); 316 | 317 | col += rle_count; 318 | } 319 | else 320 | { 321 | non_rle_buf.push_back(c); 322 | if(std::size(non_rle_buf) == 128) 323 | write_non_rle(); 324 | ++col; 325 | } 326 | } 327 | 328 | write_non_rle(); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /codecs/tga.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TGA_HPP 2 | #define TGA_HPP 3 | 4 | #include "image.hpp" 5 | 6 | class Tga final: public Image 7 | { 8 | public: 9 | Tga() = default; 10 | void open(std::istream & input, const Args & args) override; 11 | 12 | static void write(std::ostream & out, const Image & img, bool invert); 13 | }; 14 | #endif // TGA_HPP 15 | -------------------------------------------------------------------------------- /codecs/tiff.cpp: -------------------------------------------------------------------------------- 1 | #include "tiff.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | struct Tiff_io 11 | { 12 | Tiff_io() = default; 13 | 14 | explicit Tiff_io(std::istream & input): 15 | data { Image::read_input_to_memory(input) } 16 | {} 17 | 18 | void write(std::ostream & out) const 19 | { 20 | out.write(reinterpret_cast(std::data(data)), std::size(data)); 21 | } 22 | 23 | std::vector data; 24 | std::size_t pos {0}; 25 | }; 26 | 27 | tsize_t tiff_read(thandle_t hnd, tdata_t data, tsize_t size) 28 | { 29 | auto io = reinterpret_cast(hnd); 30 | auto read_size = std::min(static_cast(size), std::size(io->data) - io->pos); 31 | std::memcpy(data, std::data(io->data) + io->pos, read_size); 32 | io->pos += read_size; 33 | return read_size; 34 | } 35 | 36 | tsize_t tiff_write(thandle_t hnd, tdata_t data, tsize_t size) 37 | { 38 | auto io = reinterpret_cast(hnd); 39 | 40 | if(io->pos + size > std::size(io->data)) 41 | io->data.resize(io->pos + size); 42 | 43 | std::memcpy(std::data(io->data) + io->pos, data, size); 44 | io->pos += size; 45 | 46 | return size; 47 | } 48 | 49 | toff_t tiff_seek(thandle_t hnd, toff_t off, int whence) 50 | { 51 | auto io = reinterpret_cast(hnd); 52 | switch(whence) 53 | { 54 | case SEEK_SET: 55 | io->pos = off; 56 | break; 57 | case SEEK_CUR: 58 | io->pos += off; 59 | break; 60 | case SEEK_END: 61 | io->pos = std::size(io->data) - off; 62 | } 63 | if(io->pos > std::size(io->data)) 64 | io->data.resize(io->pos); 65 | 66 | return io->pos; 67 | } 68 | 69 | toff_t tiff_size(thandle_t hnd) 70 | { 71 | auto io = reinterpret_cast(hnd); 72 | return std::size(io->data); 73 | } 74 | 75 | void Tiff::open(std::istream & input, const Args &) 76 | { 77 | // libtiff does kind of a stupid thing and will seek backwards, which Header_stream doesn't support (because we can read from a pipe) 78 | // read the whole, huge file into memory instead 79 | Tiff_io tiff_reader(input); 80 | 81 | TIFFSetWarningHandler(nullptr); 82 | 83 | auto tiff = TIFFClientOpen("TIFF", "r", &tiff_reader, tiff_read, [](auto,auto,auto){return tsize_t{0};}, tiff_seek, [](auto){return 0;}, tiff_size, [](auto,auto,auto){return 0;}, [](auto,auto,auto){}); 84 | if(!tiff) 85 | throw std::runtime_error{"Error reading TIFF data"}; 86 | 87 | std::uint32_t w, h; 88 | TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &w); 89 | TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &h); 90 | 91 | std::vector raster(w * h); 92 | 93 | if(!TIFFReadRGBAImageOriented(tiff, w, h, std::data(raster), ORIENTATION_TOPLEFT, 0)) 94 | { 95 | TIFFClose(tiff); 96 | throw std::runtime_error{"Error reading TIFF data"}; 97 | } 98 | 99 | set_size(w, h); 100 | 101 | for(std::size_t row = 0; row < height_; ++row) 102 | { 103 | for(std::size_t col = 0; col < width_; ++col) 104 | { 105 | auto pix = raster[row * width_ + col]; 106 | image_data_[row][col].r = TIFFGetR(pix); 107 | image_data_[row][col].g = TIFFGetG(pix); 108 | image_data_[row][col].b = TIFFGetB(pix); 109 | image_data_[row][col].a = TIFFGetA(pix); 110 | } 111 | } 112 | 113 | TIFFClose(tiff); 114 | } 115 | 116 | void Tiff::write(std::ostream & out, const Image & img, bool invert) 117 | { 118 | Tiff_io tiff_writer; 119 | 120 | auto tiff = TIFFClientOpen("TIFF", "w", &tiff_writer, [](auto,auto,auto){return tsize_t{0};}, tiff_write, tiff_seek, [](auto){return 0;}, tiff_size, [](auto,auto,auto){return 0;}, [](auto,auto,auto){}); 121 | if(!tiff) 122 | throw std::runtime_error{"Error writing TIFF data"}; 123 | 124 | TIFFSetField(tiff, TIFFTAG_IMAGEWIDTH, img.get_width()); 125 | TIFFSetField(tiff, TIFFTAG_IMAGELENGTH, img.get_height()); 126 | TIFFSetField(tiff, TIFFTAG_SAMPLESPERPIXEL, 4); 127 | TIFFSetField(tiff, TIFFTAG_BITSPERSAMPLE, 8); 128 | TIFFSetField(tiff, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); 129 | TIFFSetField(tiff, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); 130 | TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); 131 | 132 | if(static_cast(TIFFScanlineSize(tiff)) != img.get_width() * 4) 133 | { 134 | TIFFClose(tiff); 135 | throw std::runtime_error{"TIFF scanline size incorrect"}; 136 | } 137 | 138 | std::vector rowbuf(img.get_width()); 139 | for(std::size_t row = 0; row < img.get_height(); ++row) 140 | { 141 | for(std::size_t col = 0; col < img.get_width(); ++col) 142 | { 143 | rowbuf[col] = img[row][col]; 144 | if(invert) 145 | rowbuf[col].invert(); 146 | } 147 | if(TIFFWriteScanline(tiff, std::data(rowbuf), row, 0) < 0) 148 | { 149 | TIFFClose(tiff); 150 | throw std::runtime_error{"Error writing TIFF data"}; 151 | } 152 | } 153 | 154 | TIFFClose(tiff); 155 | 156 | tiff_writer.write(out); 157 | } 158 | -------------------------------------------------------------------------------- /codecs/tiff.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TIFF_HPP 2 | #define TIFF_HPP 3 | 4 | #include "image.hpp" 5 | 6 | inline bool is_tiff(const Image::Header & header) 7 | { 8 | const std::array tiff_header1 = {0x49, 0x49, 0x2A, 0x00}; 9 | const std::array tiff_header2 = {0x4D, 0x4D, 0x00, 0x2A}; 10 | 11 | return std::equal(std::begin(tiff_header1), std::end(tiff_header1), std::begin(header), Image::header_cmp) || 12 | std::equal(std::begin(tiff_header2), std::end(tiff_header2), std::begin(header), Image::header_cmp); 13 | } 14 | 15 | #ifdef TIFF_FOUND 16 | class Tiff final: public Image 17 | { 18 | public: 19 | Tiff() = default; 20 | void open(std::istream & input, const Args & args) override; 21 | 22 | static void write(std::ostream & out, const Image & img, bool invert); 23 | }; 24 | #endif 25 | #endif // TIFF_HPP 26 | -------------------------------------------------------------------------------- /codecs/webp.cpp: -------------------------------------------------------------------------------- 1 | #include "webp.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | void Webp::open(std::istream & input, const Args &) 9 | { 10 | auto data = Image::read_input_to_memory(input); 11 | 12 | int width, height; 13 | if(!WebPGetInfo(reinterpret_cast(std::data(data)), std::size(data), &width, &height)) 14 | throw std::runtime_error{"Invalid WEBP header\n"}; 15 | 16 | set_size(width, height); 17 | 18 | uint8_t * pix_data = WebPDecodeRGBA(reinterpret_cast(std::data(data)), std::size(data), &width, &height); 19 | 20 | for(std::size_t row = 0; row < height_; ++row) 21 | { 22 | for(std::size_t col = 0; col < width_; ++col) 23 | { 24 | for(std::size_t i = 0; i < 4; ++i) 25 | { 26 | image_data_[row][col][i] = pix_data[4 * (row * width_ + col) + i]; 27 | } 28 | } 29 | } 30 | 31 | WebPFree(pix_data); 32 | } 33 | 34 | void Webp::write(std::ostream & out, const Image & img, bool invert) 35 | { 36 | std::vector data(img.get_width() * img.get_height() * 4); 37 | 38 | for(std::size_t row = 0; row < img.get_height(); ++row) 39 | { 40 | for(std::size_t col = 0; col < img.get_width(); ++col) 41 | { 42 | for(std::size_t i = 0; i < 4; ++i) 43 | { 44 | if(invert && i < 3) 45 | data[row * img.get_width() * 4 + col * 4 + i] = 255 - img[row][col][i]; 46 | else 47 | data[row * img.get_width() * 4 + col * 4 + i] = img[row][col][i]; 48 | } 49 | } 50 | } 51 | 52 | std::uint8_t * output; 53 | 54 | auto output_size = WebPEncodeLosslessRGBA(std::data(data), img.get_width(), img.get_width(), img.get_width() * 4, &output); 55 | 56 | out.write(reinterpret_cast(output), output_size); 57 | 58 | WebPFree(output); 59 | } 60 | -------------------------------------------------------------------------------- /codecs/webp.hpp: -------------------------------------------------------------------------------- 1 | #ifndef WEBP_HPP 2 | #define WEBP_HPP 3 | 4 | #include "image.hpp" 5 | 6 | inline bool is_webp(const Image::Header & header) 7 | { 8 | const std::array webp_header1 = {0x52, 0x49, 0x46, 0x46}; 9 | // there are 4 don't care bytes in between 10 | const std::array webp_header2 = {0x57, 0x45, 0x42, 0x50}; 11 | 12 | return std::equal(std::begin(webp_header1), std::end(webp_header1), std::begin(header), Image::header_cmp) && 13 | std::equal(std::begin(webp_header2), std::end(webp_header2), std::begin(header) + 8, Image::header_cmp); 14 | } 15 | 16 | #ifdef WEBP_FOUND 17 | class Webp final: public Image 18 | { 19 | public: 20 | Webp() = default; 21 | void open(std::istream & input, const Args & args) override; 22 | 23 | static void write(std::ostream & out, const Image & img, bool invert); 24 | }; 25 | #endif 26 | #endif // WEBP_HPP 27 | -------------------------------------------------------------------------------- /codecs/xpm.hpp: -------------------------------------------------------------------------------- 1 | #ifndef XPM_HPP 2 | #define XPM_HPP 3 | 4 | #include "image.hpp" 5 | 6 | #ifdef XPM_FOUND 7 | class Xpm final: public Image 8 | { 9 | public: 10 | Xpm() = default; 11 | void open(std::istream & input, const Args & args) override; 12 | 13 | static void write(std::ostream & out, const Image & img, bool invert); 14 | }; 15 | #endif 16 | #endif // XPM_HPP 17 | -------------------------------------------------------------------------------- /color.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COLOR_HPP 2 | #define COLOR_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | // from boost::hash_combine 12 | inline std::size_t hash_combine(std::size_t a, std::size_t b) 13 | { 14 | if constexpr(sizeof(std::size_t) == 8) 15 | a^= b + 0x9e3779b97f4a7c15ull + (a << 6) + (a >> 2); 16 | else 17 | a^= b + 0x9e3779b9ul + (a << 6) + (a >> 2); 18 | return a; 19 | } 20 | 21 | struct Color 22 | { 23 | unsigned char r{0}, g{0}, b{0}, a{0xFF}; 24 | constexpr Color(){} 25 | constexpr Color(unsigned char r, unsigned char g, unsigned char b, unsigned char a = 0xFF): r{r}, g{g}, b{b}, a{a} {} 26 | constexpr explicit Color(unsigned char y): r{y}, g{y}, b{y} {} 27 | 28 | constexpr const unsigned char & operator[](unsigned char i) const 29 | { 30 | switch(i) 31 | { 32 | case 0: 33 | return r; 34 | case 1: 35 | return g; 36 | case 2: 37 | return b; 38 | case 3: 39 | return a; 40 | default: 41 | throw std::logic_error{"color index out of bounds"}; 42 | } 43 | } 44 | constexpr unsigned char & operator[](unsigned char i) 45 | { 46 | switch(i) 47 | { 48 | case 0: 49 | return r; 50 | case 1: 51 | return g; 52 | case 2: 53 | return b; 54 | case 3: 55 | return a; 56 | default: 57 | throw std::logic_error{"color index out of bounds"}; 58 | } 59 | } 60 | 61 | Color & invert() 62 | { 63 | r = 255 - r; 64 | g = 255 - g; 65 | b = 255 - b; 66 | 67 | return *this; 68 | } 69 | 70 | constexpr bool operator<(const Color & other) const 71 | { 72 | if(r != other.r) 73 | return r < other.r; 74 | else if(g != other.g) 75 | return g < other.g; 76 | else if(b != other.b) 77 | return b < other.b; 78 | else 79 | return a < other.a; 80 | } 81 | 82 | constexpr bool operator==(const Color & other) const 83 | { 84 | return r == other.r 85 | && g == other.g 86 | && b == other.b 87 | && a == other.a; 88 | } 89 | 90 | Color & operator+=(const Color & other) 91 | { 92 | r += other.r; 93 | g += other.g; 94 | b += other.b; 95 | a += other.a; 96 | return *this; 97 | } 98 | Color & operator-=(const Color & other) 99 | { 100 | r -= other.r; 101 | g -= other.g; 102 | b -= other.b; 103 | a -= other.a; 104 | return *this; 105 | } 106 | 107 | Color & operator*=(decltype(r) other) 108 | { 109 | r *= other; 110 | g *= other; 111 | b *= other; 112 | a *= other; 113 | return *this; 114 | } 115 | Color & operator/=(decltype(r) other) 116 | { 117 | r /= other; 118 | g /= other; 119 | b /= other; 120 | a /= other; 121 | return *this; 122 | } 123 | Color & operator%=(decltype(r) other) 124 | { 125 | r %= other; 126 | g %= other; 127 | b %= other; 128 | a %= other; 129 | return *this; 130 | } 131 | 132 | Color operator+(const Color & other) const 133 | { 134 | return Color{*this} += other; 135 | } 136 | Color operator-(const Color & other) const 137 | { 138 | return Color{*this} -= other; 139 | } 140 | 141 | Color operator*(decltype(r) other) const 142 | { 143 | return Color{*this} *= other; 144 | } 145 | Color operator/(decltype(r) other) const 146 | { 147 | return Color{*this} /= other; 148 | } 149 | Color operator%(decltype(r) other) const 150 | { 151 | return Color{*this} %= other; 152 | } 153 | }; 154 | 155 | struct FColor 156 | { 157 | float r{0.0f}, g{0.0f}, b{0.0f}, a{1.0f}; 158 | constexpr FColor(){} 159 | constexpr FColor(float r, float g, float b, float a = 1.0f): 160 | r{r}, 161 | g{g}, 162 | b{b}, 163 | a{a} 164 | {} 165 | constexpr FColor(const Color & c): 166 | r{c.r / 255.0f}, 167 | g{c.g / 255.0f}, 168 | b{c.b / 255.0f}, 169 | a{c.a / 255.0f} 170 | {} 171 | 172 | constexpr operator Color() const 173 | { 174 | return { 175 | static_cast(r * 255.0f), 176 | static_cast(g * 255.0f), 177 | static_cast(b * 255.0f), 178 | static_cast(a * 255.0f) 179 | }; 180 | } 181 | 182 | constexpr bool operator<(const FColor & other) const 183 | { 184 | if(r != other.r) 185 | return r < other.r; 186 | else if(g != other.g) 187 | return g < other.g; 188 | else if(b != other.b) 189 | return b < other.b; 190 | else 191 | return a < other.a; 192 | } 193 | 194 | FColor & alpha_blend(float bg) 195 | { 196 | r = r * a + bg * (1.0f - a); 197 | g = g * a + bg * (1.0f - a); 198 | b = b * a + bg * (1.0f - a); 199 | a = 1.0f; 200 | return *this; 201 | } 202 | float to_gray() const 203 | { 204 | // formulas from https://www.w3.org/TR/WCAG20/ 205 | std::array luminance_color = { r, g, b }; 206 | 207 | for(auto && c: luminance_color) 208 | c = (c <= 0.03928f) ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f); 209 | 210 | return 0.2126f * luminance_color[0] + 0.7152f * luminance_color[1] + 0.0722f * luminance_color[2]; 211 | } 212 | FColor & invert() 213 | { 214 | r = 1.0f - r; 215 | g = 1.0f - g; 216 | b = 1.0f - b; 217 | 218 | return *this; 219 | } 220 | 221 | FColor & operator+=(const FColor & other) 222 | { 223 | r += other.r; 224 | g += other.g; 225 | b += other.b; 226 | a += other.a; 227 | return *this; 228 | } 229 | FColor & operator-=(const FColor & other) 230 | { 231 | r -= other.r; 232 | g -= other.g; 233 | b -= other.b; 234 | a -= other.a; 235 | return *this; 236 | } 237 | 238 | FColor & operator*=(float other) 239 | { 240 | r *= other; 241 | g *= other; 242 | b *= other; 243 | a *= other; 244 | return *this; 245 | } 246 | FColor & operator/=(float other) 247 | { 248 | r /= other; 249 | g /= other; 250 | b /= other; 251 | a /= other; 252 | return *this; 253 | } 254 | 255 | FColor operator+(const FColor & other) const 256 | { 257 | return FColor{*this} += other; 258 | } 259 | FColor operator-(const FColor & other) const 260 | { 261 | return FColor{*this} -= other; 262 | } 263 | 264 | FColor operator*(decltype(r) other) const 265 | { 266 | return FColor{*this} *= other; 267 | } 268 | FColor operator/(decltype(r) other) const 269 | { 270 | return FColor{*this} /= other; 271 | } 272 | 273 | FColor & clamp() 274 | { 275 | r = std::clamp(r, 0.0f, 1.0f); 276 | g = std::clamp(g, 0.0f, 1.0f); 277 | b = std::clamp(b, 0.0f, 1.0f); 278 | a = std::clamp(a, 0.0f, 1.0f); 279 | return *this; 280 | } 281 | }; 282 | 283 | namespace std 284 | { 285 | template<> struct hash 286 | { 287 | auto operator()(const Color & c) const 288 | { 289 | return hash_combine(hash_combine(hash_combine(hash{}(c.r), hash{}(c.g)), hash{}(c.b)), hash{}(c.a)); 290 | } 291 | }; 292 | template<> struct hash 293 | { 294 | auto operator()(const FColor & c) const 295 | { 296 | return hash_combine(hash_combine(hash_combine(hash{}(c.r), hash{}(c.g)), hash{}(c.b)), hash{}(c.a)); 297 | } 298 | }; 299 | } 300 | 301 | inline float color_dist2(const FColor & a, const FColor & b) 302 | { 303 | return (a.r - b.r) * (a.r - b.r) + (a.g - b.g) * (a.g - b.g) + (a.b - b.b) * (a.b - b.b) + (a.a - b.a) * (a.a - b.a); 304 | } 305 | 306 | inline float color_dist(const FColor & a, const FColor & b) 307 | { 308 | return std::sqrt(color_dist2(a, b)); 309 | } 310 | 311 | #endif // COLOR_HPP 312 | -------------------------------------------------------------------------------- /config.h.in: -------------------------------------------------------------------------------- 1 | #cmakedefine FONTCONFIG_FOUND 1 2 | #cmakedefine FREETYPE_FOUND 1 3 | 4 | #cmakedefine AVIF_FOUND 1 5 | #cmakedefine BPG_FOUND 1 6 | #cmakedefine EXIF_FOUND 1 7 | #cmakedefine FLIF_ENC_FOUND 1 8 | #cmakedefine FLIF_DEC_FOUND 1 9 | #cmakedefine GIF_FOUND 1 10 | #cmakedefine HEIF_FOUND 1 11 | #cmakedefine JP2_FOUND 1 12 | #cmakedefine JPEG_FOUND 1 13 | #cmakedefine JXL_FOUND 1 14 | #cmakedefine MNG_FOUND 1 15 | #cmakedefine OpenEXR_FOUND 1 16 | #cmakedefine PNG_FOUND 1 17 | #cmakedefine SVG_FOUND 1 18 | #cmakedefine TIFF_FOUND 1 19 | #cmakedefine WEBP_FOUND 1 20 | #cmakedefine XPM_FOUND 1 21 | #cmakedefine ZLIB_FOUND 1 22 | 23 | #cmakedefine HAS_SIGNAL 1 24 | #cmakedefine HAS_IOCTL 1 25 | #cmakedefine HAS_SELECT 1 26 | #cmakedefine HAS_TERMIOS 1 27 | #cmakedefine HAS_UNISTD 1 28 | -------------------------------------------------------------------------------- /cxxopts_wrapper.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CXXOPTS_WRAPPER_HPP 2 | #define CXXOPTS_WRAPPER_HPP 3 | 4 | #include 5 | 6 | // the following addresses a breaking change in the 3.0 branch of cxxopts that 7 | // isn't detectable by version number. 8 | // prior versions use cxxopts::OptionException, later use cxxopts::exceptions::exception 9 | // this should detect whichever is available and use that, falling back to std::exception 10 | 11 | // (incomplete) forward declarations 12 | namespace cxxopts 13 | { 14 | struct OptionException; 15 | namespace exceptions 16 | { 17 | struct exception; 18 | } 19 | } 20 | 21 | namespace detail 22 | { 23 | // is_type_complete technique from Raymond Chen https://devblogs.microsoft.com/oldnewthing/20190711-00/?p=102682 24 | template 25 | inline constexpr bool is_type_complete_v = false; 26 | 27 | template 28 | inline constexpr bool is_type_complete_v > = true; 29 | 30 | template 31 | struct cxxopt_exception_detect 32 | { 33 | using type = std::exception; 34 | }; 35 | 36 | template requires is_type_complete_v 37 | struct cxxopt_exception_detect 38 | { 39 | using type = cxxopts::OptionException; 40 | }; 41 | 42 | template requires is_type_complete_v 43 | struct cxxopt_exception_detect 44 | { 45 | using type = cxxopts::exceptions::exception; 46 | }; 47 | } 48 | 49 | using cxxopt_exception = detail::cxxopt_exception_detect::type; 50 | 51 | #endif // CXXOPTS_WRAPPER_HPP 52 | -------------------------------------------------------------------------------- /display.cpp: -------------------------------------------------------------------------------- 1 | #include "display.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include "animate.hpp" 15 | #include "color.hpp" 16 | #include "font.hpp" 17 | 18 | #define ESC "\x1B" 19 | #define CSI ESC "[" 20 | #define SEP ";" 21 | #define SGR "m" 22 | #define RESET_CHAR CSI "0" SGR 23 | #define FG24 "38;2;" 24 | #define BG24 "48;2;" 25 | #define FG8 "38;5;" 26 | #define BG8 "48;5;" 27 | #define UPPER_HALF_BLOCK "▀"; 28 | 29 | namespace 30 | { 31 | constexpr auto build_color_table() 32 | { 33 | // Color codes as defined in https://en.wikipedia.org/wiki/ANSI_escape_code 34 | std::array table; 35 | std::array table_4bit 36 | { 37 | Color{ 0, 0, 0, 255}, 38 | Color{170, 0, 0, 255}, 39 | Color{ 0, 170, 0, 255}, 40 | Color{170, 85, 0, 255}, 41 | Color{ 0, 0, 170, 255}, 42 | Color{170, 0, 170, 255}, 43 | Color{ 0, 170, 170, 255}, 44 | Color{170, 170, 170, 255}, 45 | Color{ 85, 85, 85, 255}, 46 | Color{255, 85, 85, 255}, 47 | Color{ 85, 255, 85, 255}, 48 | Color{255, 255, 85, 255}, 49 | Color{ 85, 85, 255, 255}, 50 | Color{255, 85, 255, 255}, 51 | Color{ 85, 255, 255, 255}, 52 | Color{255, 255, 255, 255} 53 | }; 54 | 55 | static_assert(std::size(table_4bit) <= std::size(table)); 56 | 57 | std::size_t i = 0; 58 | for(; i < std::size(table_4bit); ++i) 59 | table[i] = table_4bit[i]; 60 | 61 | std::array levels {0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF}; 62 | 63 | for(unsigned char r = 0; r < 6; ++r) 64 | { 65 | for(unsigned char g = 0; g < 6; ++g) 66 | { 67 | for(unsigned char b = 0; b < 6; ++b) 68 | { 69 | table[i++] = Color{levels[r], levels[g], levels[b], 255}; 70 | } 71 | } 72 | } 73 | 74 | for(unsigned char y = 0x08; y <= 0xee; y += 0x0A) 75 | table[i++] = Color {y, y, y, 255}; 76 | 77 | return table; 78 | }; 79 | 80 | constexpr auto color_table = build_color_table(); 81 | 82 | class set_color 83 | { 84 | public: 85 | explicit set_color(const std::optional & fg_color, const std::optional & bg_color, Args::Color color_type) 86 | { 87 | if(color_type != Args::Color::NONE && color_type != Args::Color::ANSI4 && color_type != Args::Color::ANSI8 && color_type != Args::Color::ANSI24) 88 | throw std::runtime_error{"Unsupported set_color mode"}; 89 | 90 | enum class Color_mode {none, fg_only, bg_only, both} color_mode; 91 | 92 | if(!fg_color && !bg_color) 93 | color_mode = Color_mode::none; 94 | 95 | else if(fg_color && !bg_color) 96 | color_mode = Color_mode::fg_only; 97 | 98 | else if(!fg_color && fg_color) 99 | color_mode = Color_mode::bg_only; 100 | 101 | else 102 | color_mode = Color_mode::both; 103 | 104 | std::ostringstream os; 105 | 106 | if(color_type == Args::Color::ANSI24) 107 | { 108 | switch(color_mode) 109 | { 110 | case Color_mode::none: 111 | break; 112 | case Color_mode::fg_only: 113 | os << CSI FG24 << static_cast(fg_color->r) << SEP << static_cast(fg_color->g) << SEP << static_cast(fg_color->b) <(bg_color->r) << SEP << static_cast(bg_color->g) << SEP << static_cast(bg_color->b) << SGR; 117 | break; 118 | case Color_mode::both: 119 | os << CSI FG24 << static_cast(fg_color->r) << SEP << static_cast(fg_color->g) << SEP << static_cast(fg_color->b) 120 | << SEP BG24 << static_cast(bg_color->r) << SEP << static_cast(bg_color->g) << SEP << static_cast(bg_color->b) << SGR; 121 | break; 122 | } 123 | } 124 | else if(color_type == Args::Color::ANSI8) 125 | { 126 | switch(color_mode) 127 | { 128 | case Color_mode::none: 129 | break; 130 | case Color_mode::fg_only: 131 | os << CSI FG8 << std::distance(std::begin(color_table), std::find(std::begin(color_table), std::end(color_table), *fg_color)) << SGR; 132 | break; 133 | case Color_mode::bg_only: 134 | os << CSI BG8<< std::distance(std::begin(color_table), std::find(std::begin(color_table), std::end(color_table), *bg_color)) << SGR; 135 | break; 136 | case Color_mode::both: 137 | os << CSI FG8 << std::distance(std::begin(color_table), std::find(std::begin(color_table), std::end(color_table), *fg_color)) 138 | << SEP BG8 << std::distance(std::begin(color_table), std::find(std::begin(color_table), std::end(color_table), *bg_color)) << SGR; 139 | break; 140 | } 141 | } 142 | else if(color_type == Args::Color::ANSI4) 143 | { 144 | std::array index {}; 145 | 146 | if(fg_color) 147 | index[0] = std::distance(std::begin(color_table), std::find(std::begin(color_table), std::begin(color_table) + 16, *fg_color)); 148 | if(bg_color) 149 | index[1] = std::distance(std::begin(color_table), std::find(std::begin(color_table), std::begin(color_table) + 16, *bg_color)); 150 | 151 | for(auto && i: index) 152 | { 153 | if(i >= 8 && i < 16) 154 | i += 60 - 8; 155 | else if(i >= 17) 156 | throw std::logic_error{"ASNI4 index out of range"}; 157 | } 158 | 159 | index[0] += 30; 160 | index[1] += 40; 161 | 162 | switch(color_mode) 163 | { 164 | case Color_mode::none: 165 | break; 166 | case Color_mode::fg_only: 167 | os << CSI << index[0] << SGR; 168 | break; 169 | case Color_mode::bg_only: 170 | os << CSI << index[1] << SGR; 171 | break; 172 | case Color_mode::both: 173 | os << CSI << index[0] 174 | << SEP << index[1] << SGR; 175 | break; 176 | } 177 | } 178 | 179 | command_ = os.str(); 180 | } 181 | 182 | friend std::ostream & operator<<(std::ostream &os, const set_color & sc) 183 | { 184 | return os << sc.command_; 185 | } 186 | 187 | private: 188 | std::string command_; 189 | }; 190 | 191 | std::ostream & clear_color(std::ostream & os) 192 | { 193 | return os << RESET_CHAR; 194 | } 195 | } 196 | 197 | void display_image(const Image & img, const Args & args) 198 | { 199 | if(args.animate) 200 | { 201 | auto animator = Animate{args}; 202 | do 203 | { 204 | for(auto f = 0u; f < img.num_frames(); ++f) 205 | { 206 | animator.set_frame_delay(args.animation_frame_delay > 0.0f ? std::chrono::duration(args.animation_frame_delay) : img.get_frame_delay(f)); 207 | animator.display(img.get_frame(f)); 208 | if(!animator) 209 | break; 210 | } 211 | } while(animator && args.loop_animation); 212 | } 213 | else 214 | { 215 | std::ofstream output_file; 216 | if(args.output_filename != "-") 217 | output_file.open(args.output_filename); 218 | std::ostream & out = args.output_filename == "-" ? std::cout : output_file; 219 | 220 | if(!out) 221 | throw std::runtime_error{"Could not open output file " + (args.output_filename == "-" ? "" : ("(" + args.output_filename + ") ")) + ": " + std::string{std::strerror(errno)}}; 222 | 223 | if(args.frame_no) 224 | print_image(img.get_frame(*args.frame_no), args, out); 225 | else 226 | print_image(img.get_image(args.image_no.value_or(0u)), args, out); 227 | } 228 | } 229 | 230 | void print_image(const Image & img, const Args & args, std::ostream & out) 231 | { 232 | if(img.get_width() == 0 || img.get_height() == 0) 233 | return; 234 | 235 | Char_vals char_vals; 236 | if(args.disp_char == Args::Disp_char::ASCII) 237 | { 238 | auto font_path = get_font_path(args.font_name); 239 | char_vals = get_char_values(font_path, args.font_size); 240 | } 241 | 242 | int rows = 0, cols = 0; 243 | 244 | if(!args.cols) 245 | cols = std::min({80, get_screen_cols(), static_cast(img.get_width())}); 246 | else 247 | cols = *args.cols; 248 | 249 | if(!args.rows) 250 | rows = -1; 251 | else 252 | rows = *args.rows; 253 | 254 | const auto bg = args.bg / 255.0f; 255 | auto disp_height = rows > 0 ? rows : img.get_height() * cols / img.get_width() / 2; 256 | if(args.disp_char == Args::Disp_char::HALF_BLOCK) 257 | disp_height *= 2; 258 | 259 | auto scaled_img = img.scale(cols, disp_height); 260 | 261 | for(std::size_t row = 0; row < scaled_img.get_height(); ++row) 262 | { 263 | for(std::size_t col = 0; col < scaled_img.get_width(); ++col) 264 | { 265 | FColor disp_c {scaled_img[row][col]}; 266 | 267 | if(args.invert) 268 | disp_c.invert(); 269 | 270 | disp_c.alpha_blend(bg); 271 | 272 | scaled_img[row][col] = disp_c; 273 | } 274 | } 275 | 276 | if(args.color == Args::Color::ANSI8) 277 | scaled_img.dither(std::begin(color_table), std::end(color_table)); 278 | else if(args.color == Args::Color::ANSI4) 279 | scaled_img.dither(std::begin(color_table), std::begin(color_table) + 16); 280 | 281 | for(std::size_t row = 0; row < (args.disp_char == Args::Disp_char::HALF_BLOCK ? scaled_img.get_height() / 2 : scaled_img.get_height()); ++row) 282 | { 283 | for(std::size_t col = 0; col < scaled_img.get_width(); ++col) 284 | { 285 | switch(args.disp_char) 286 | { 287 | case Args::Disp_char::HALF_BLOCK: 288 | out<(FColor{color}.to_gray() * 255.0f)]; 299 | out< 4 | #include 5 | #include 6 | 7 | #include "config.h" 8 | 9 | #if defined(FONTCONFIG_FOUND) && defined(FREETYPE_FOUND) 10 | #include 11 | #include 12 | #include FT_FREETYPE_H 13 | #endif 14 | 15 | [[nodiscard]] std::string get_font_path(const std::string & font_name) 16 | { 17 | #if defined(FONTCONFIG_FOUND) && defined(FREETYPE_FOUND) 18 | struct Fontconfig 19 | { 20 | FcConfig * config {nullptr}; 21 | Fontconfig() 22 | { 23 | if(!FcInit()) 24 | throw std::runtime_error{"Error loading fontconfig library"}; 25 | config = FcInitLoadConfigAndFonts(); 26 | } 27 | ~Fontconfig() 28 | { 29 | FcConfigDestroy(config); 30 | FcFini(); 31 | } 32 | operator FcConfig *() { return config; } 33 | operator const FcConfig *() const { return config; } 34 | }; 35 | 36 | Fontconfig fc; 37 | 38 | // get a list of fonts matching the given name 39 | struct Pattern 40 | { 41 | FcPattern * pat{nullptr}; 42 | explicit Pattern(FcPattern * pat): pat{pat} {} 43 | ~Pattern() { if(pat) FcPatternDestroy(pat); } 44 | operator FcPattern*() { return pat; } 45 | operator const FcPattern*() const { return pat; } 46 | }; 47 | 48 | Pattern font_pat {FcNameParse(reinterpret_cast(font_name.c_str()))}; 49 | FcConfigSubstitute(fc, font_pat, FcMatchPattern); 50 | FcDefaultSubstitute(font_pat); 51 | 52 | struct FontSet 53 | { 54 | FcFontSet * set{nullptr}; 55 | explicit FontSet(FcFontSet * set): set{set} {} 56 | ~FontSet() { if(set) FcFontSetDestroy(set); } 57 | operator const FcFontSet*() const { return set; } 58 | operator FcFontSet*() { return set; } 59 | FcFontSet * operator->() { return set; }; 60 | const FcFontSet * operator->() const { return set; }; 61 | FcPattern* operator[](int i) { return set->fonts[i]; } 62 | const FcPattern* operator[](int i) const { return set->fonts[i]; } 63 | }; 64 | 65 | FcResult result; 66 | FontSet fonts {FcFontSort(fc, font_pat, false, NULL, &result)}; 67 | if(result != FcResultMatch) 68 | throw std::runtime_error{"Error finding font matching: " + font_name}; 69 | 70 | for(int i = 0; i < fonts->nfont; ++i) 71 | { 72 | // filter out any fonts that aren't monospaced 73 | int spacing; 74 | if(FcPatternGetInteger(fonts[i], FC_SPACING, 0, &spacing) != FcResultMatch) 75 | continue; 76 | 77 | if(spacing != FC_MONO) 78 | continue; 79 | 80 | FcChar8 * font_path; 81 | if(FcPatternGetString(fonts[i], FC_FILE, 0, &font_path) != FcResultMatch) 82 | throw std::runtime_error{"Could not get path to: " + font_name}; 83 | 84 | return {reinterpret_cast(font_path)}; 85 | } 86 | 87 | throw std::runtime_error{"No fonts found matching: " + font_name}; 88 | #else 89 | // supress unused parameter warnings 90 | (void)font_name; 91 | 92 | return {}; 93 | #endif 94 | } 95 | 96 | [[nodiscard]] Char_vals get_char_values(const std::string & font_path, float font_size) 97 | { 98 | #if defined(FONTCONFIG_FOUND) && defined(FREETYPE_FOUND) 99 | struct Freetype 100 | { 101 | FT_Library lib {nullptr}; 102 | Freetype() 103 | { 104 | if(FT_Init_FreeType(&lib) != FT_Err_Ok) 105 | throw std::runtime_error{"Error loading Freetype library"}; 106 | } 107 | ~Freetype() { if(lib) FT_Done_FreeType(lib); } 108 | operator FT_Library() { return lib; } 109 | operator FT_Library() const { return lib; } 110 | }; 111 | 112 | struct Face 113 | { 114 | FT_Face face {nullptr}; 115 | Face(FT_Library ft, const std::string & font_path) 116 | { 117 | if(FT_New_Face(ft, font_path.c_str(), 0, &face) != FT_Err_Ok) 118 | throw std::runtime_error{"Error opening font file: " + font_path}; 119 | if(!face->charmap) 120 | throw std::runtime_error{"Error font does not contain unicode charmap"}; 121 | } 122 | ~Face() { if(face) FT_Done_Face(face); } 123 | operator FT_Face() { return face; } 124 | operator FT_Face() const { return face; } 125 | FT_Face operator->() { return face; } 126 | FT_Face operator->() const { return face; } 127 | }; 128 | 129 | Freetype ft; 130 | Face face(ft, font_path); 131 | 132 | if(FT_Set_Char_Size(face, 0, static_cast(64.0f * font_size), 0, 0) != FT_Err_Ok) 133 | throw std::runtime_error{"Error setting font size: " + std::to_string(font_size)}; 134 | 135 | Char_vals char_vals; 136 | auto char_width = FT_MulFix(face->max_advance_width, face->size->metrics.x_scale) / 64; 137 | auto char_height = FT_MulFix(face->height, face->size->metrics.y_scale) / 64; 138 | std::vector> values; 139 | 140 | for(char ch = ' '; ch <= '~'; ++ch) 141 | { 142 | if(FT_Load_Char(face, ch, FT_LOAD_RENDER) != FT_Err_Ok) 143 | throw std::runtime_error{"Error loading char: " + std::string{ch}}; 144 | 145 | auto & bmp = face->glyph->bitmap; 146 | 147 | float sum {0.0f}; 148 | 149 | for(std::size_t y = 0; y < static_cast(bmp.rows); ++y) 150 | { 151 | for(std::size_t x = 0; x < static_cast(bmp.width); ++x) 152 | { 153 | sum += bmp.buffer[y * bmp.width +x]; 154 | } 155 | } 156 | 157 | values.emplace_back(ch, sum / (char_width * char_height)); 158 | } 159 | 160 | std::sort(std::begin(values), std::end(values), [](const auto & a, const auto & b) { return a.second < b.second; }); 161 | 162 | // change value range to 0-255, and assign a char for each number in that range 163 | auto min = values.front().second; 164 | auto max = values.back().second; 165 | auto range = max - min; 166 | 167 | for(std::size_t i = 0, j = 0; i < std::size(char_vals); ++i) 168 | { 169 | if((values[j].second * 255 / range + min) < i && j < std::size(values) - 1) 170 | ++j; 171 | 172 | char_vals[i] = values[j].first; 173 | } 174 | 175 | return char_vals; 176 | #else 177 | // supress unused parameter warnings 178 | (void)font_path, (void)font_size; 179 | // if we don't have fontconfig / freetype, use this pregenerated list instead 180 | return 181 | { 182 | ' ', '`', '`', '`', '`', '`', '`', '`', '`', '`', '`', '`', '`', '`', 183 | '`', '`', '`', '`', '`', '.', '.', '.', '.', '.', '.', '.', '.', '-', 184 | '\'', ',', ',', ',', ',', ',', ',', ':', ':', ':', ':', ':', ':', ':', 185 | ':', ':', ':', ':', ':', ':', ':', ':', ':', ':', ':', '"', '"', '~', 186 | '^', '^', ';', ';', ';', '_', '_', '_', '!', '!', '!', '!', '!', '!', 187 | '!', '!', '!', '!', '!', '!', '!', '!', '!', '!', '!', '!', '*', '*', 188 | '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '\\', 189 | '\\', '/', 'r', '(', '(', ')', '|', '|', '+', '>', '<', '=', '?', 'c', 190 | 'c', 'c', 'c', 'c', 'c', 'l', 'l', 'l', 'l', 'l', 'l', 'l', 'l', 'i', 191 | 'i', 'i', 'i', '[', ']', 'v', 's', 'L', '7', 'j', 'z', 'x', 'x', 't', 192 | 'J', '}', '{', 'T', 'Y', '1', 'f', 'C', 'n', 'n', 'n', 'n', 'n', 'u', 193 | 'I', 'I', 'I', 'I', 'I', 'o', 'o', '2', 'F', '3', '3', '3', 'S', 'S', 194 | 'S', 'y', 'y', 'y', '5', 'V', 'e', 'a', 'w', 'w', 'w', 'Z', 'h', 'h', 195 | '4', 'X', '%', 'k', 'P', 'P', '$', '$', 'G', 'G', 'G', 'G', 'G', 'U', 196 | 'U', 'U', 'E', 'E', '&', 'm', 'b', 'b', 'b', 'b', 'd', '9', 'p', 'q', 197 | 'A', '6', 'O', 'K', '#', '0', 'H', 'H', '8', '8', '8', '8', '8', 'D', 198 | 'D', 'g', 'g', 'R', 'Q', 'Q', 'Q', 'Q', '@', '@', '@', '@', '@', '@', 199 | '@', '@', '@', '@', '@', '@', '@', '@', '@', 'B', 'B', 'B', 'N', 'W', 200 | 'W', 'M', 'M', 'M' 201 | }; 202 | #endif 203 | } 204 | -------------------------------------------------------------------------------- /font.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FONT_HPP 2 | #define FONT_HPP 3 | 4 | #include 5 | #include 6 | 7 | using Char_vals = std::array; 8 | 9 | [[nodiscard]] std::string get_font_path(const std::string & font_name); 10 | [[nodiscard]] Char_vals get_char_values(const std::string & font_path, float font_size); 11 | 12 | #endif // FONT_HPP 13 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | // Convert an image input to ascii art 2 | #include 3 | 4 | #include "display.hpp" 5 | #include "codecs/image.hpp" 6 | 7 | int main(int argc, char * argv[]) 8 | { 9 | auto args = parse_args(argc, argv); 10 | if(!args) 11 | return EXIT_FAILURE; 12 | 13 | try 14 | { 15 | auto img = get_image_data(*args); 16 | 17 | if(args->get_image_count) 18 | { 19 | std::cout<num_images()<<'\n'; 20 | return 0; 21 | } 22 | 23 | if(args->get_frame_count) 24 | { 25 | std::cout<num_frames()<<'\n'; 26 | return 0; 27 | } 28 | 29 | if(!img->supports_multiple_images() && args->image_no > 0) 30 | throw std::runtime_error{args->help_text + "\nImage type doesn't support multiple images"}; 31 | 32 | if(!img->supports_animation() && args->animate) 33 | throw std::runtime_error{args->help_text + "\nImage type doesn't support animation"}; 34 | 35 | if(args->display) 36 | display_image(*img, *args); 37 | 38 | if(args->convert_filename) 39 | { 40 | if(args->frame_no) 41 | img->get_frame(*args->frame_no).convert(*args); 42 | else if(args->image_no) 43 | img->get_image(*args->image_no).convert(*args); 44 | else 45 | img->convert(*args); 46 | } 47 | } 48 | catch(Early_exit & e) 49 | { 50 | return EXIT_SUCCESS; 51 | } 52 | catch(const std::runtime_error & e) 53 | { 54 | std::cerr<