├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── pull_request_template.md └── workflows │ ├── ci.yml │ └── cimg.yml ├── .gitignore ├── LICENSE ├── README.md ├── snapcraft.yaml ├── src ├── CImg.h ├── Makefile ├── java │ └── TerminalImageViewer.java ├── tiv.cpp ├── tiv_lib.cpp └── tiv_lib.h └── terminalimageviewer.rb /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a report to help us improve 3 | labels: bug 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Describe the Bug 8 | description: A clear and concise description of what the bug is. 9 | validations: 10 | required: true 11 | - type: textarea 12 | attributes: 13 | label: To Reproduce 14 | description: Steps to reproduce the behavio(u)r 15 | value: |- 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | validations: 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: Expected Behavio(u)r 25 | description: A clear and concise description of what you expected to happen. 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: Screenshots 31 | description: If applicable, add screenshots to help explain your problem. 32 | - type: input 33 | attributes: 34 | label: Version/Commit Hash 35 | validations: 36 | required: true 37 | - type: input 38 | attributes: 39 | label: OS Specifics 40 | description: Output of `uname -a` or the Windows build number 41 | validations: 42 | required: true 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: "Question" 4 | url: https://github.com/stefanhaustein/TerminalImageViewer/discussions/new?category=q-a 5 | about: Ask things about the project 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | labels: feature request 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Is your feature request related to a problem? Please describe. 8 | description: A clear and concise description of what the bug is. 9 | - type: textarea 10 | attributes: 11 | label: Describe the solution you'd like 12 | description: A clear and concise description of what you want to happen. 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: Describe alternatives you've considered 18 | description: A clear and concise description of any alternative solutions or features you've considered. 19 | validations: 20 | required: true 21 | - type: textarea 22 | attributes: 23 | label: Additional context 24 | description: Add any other context or screenshots about the feature request here. 25 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ### How can we test this? Step-by-step instructions, please. 6 | 7 | 1. 8 | 2. 9 | 3. 10 | 11 | ### What happened before? 12 | 13 | 14 | ### What should happen with this fix? 15 | 16 | 17 | ### Anything else we should know about this patch? 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and run 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: ["master"] 7 | paths: ["src/**"] 8 | pull_request: 9 | paths: ["src/**"] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Install dependencies 18 | run: sudo apt-get install -qy imagemagick libpng-dev 19 | - name: Validate gcc version 20 | run: | 21 | if [[ $(gcc --version | awk '/gcc/ && ($3+0)>13{print "gcc-13+"}') != "gcc-13+" ]]; then 22 | # Script courtesy of https://stackoverflow.com/a/67791068/16134571 23 | sudo apt install gcc-13 g++-13 24 | sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100 --slave /usr/bin/g++ g++ /usr/bin/g++-13 --slave /usr/bin/gcov gcov /usr/bin/gcov-13 25 | sudo update-alternatives --set gcc /usr/bin/gcc-13 26 | fi 27 | - name: Build 28 | run: make -C src 29 | - name: Test 30 | run: | 31 | images=('/usr/local/share/icons/hicolor/128x128/apps/microsoft-edge.png' '/usr/local/share/icons/hicolor/128x128/apps/CMakeSetup.png' '/usr/local/doc/cmake/html/_static/file.png' '/usr/local/lib/android/sdk/extras/google/google_play_services/samples/tagmanager/cuteanimals/res/drawable/cat_1.jpg' '/usr/local/lib/android/sdk/extras/google/google_play_services/samples/wallet/res/drawable-ldpi/icon.png' '/usr/local/lib/android/sdk/extras/google/google_play_services/samples/wallet/res/drawable-hdpi/icon.png' '/usr/share/plymouth/themes/spinner/watermark.png' '/usr/share/apache2/icons/apache_pb.png' '/usr/share/doc/libpng-dev/examples/pngtest.png') 32 | image=${images[ $RANDOM % ${#images[@]} ]} # Get random image 33 | ./src/tiv -w 160 -h 48 $image # Get random image 34 | echo $image 35 | ./src/tiv -w 160 -h 48 /usr/share/pixmaps # Dir mode 36 | -------------------------------------------------------------------------------- /.github/workflows/cimg.yml: -------------------------------------------------------------------------------- 1 | name: Update CImg 2 | on: 3 | schedule: 4 | - cron: 0 0 3 * * # The first day after this workflow was merged 5 | workflow_dispatch: 6 | jobs: 7 | check-version: 8 | name: Check for updates 9 | runs-on: ubuntu-latest 10 | outputs: 11 | latest-tag: ${{ steps.latest-tag.outputs.result }} 12 | steps: 13 | - name: Fetch the latest tag (could be buggy) 14 | uses: actions/github-script@v7 15 | id: latest-tag 16 | with: 17 | result-encoding: string 18 | script: | 19 | return (await (Promise.all( [ github.rest.repos.listTags({ owner: 'GreycLab', repo: 'CImg', per_page: 28 }), 20 | github.rest.repos.listCommits({ owner: 'stefanhaustein', repo: 'TerminalImageViewer', per_page: 1, author: 'actions-user' }) 21 | ] ).then( ( values ) => { 22 | console.log( values[0].data[27].name ); 23 | console.log( values[1].data[0].commit.message.slice( 15 ) ); 24 | if( values[0].data[27].name === values[1].data[0].commit.message.slice( 15 ) ) return ""; 25 | return values[0].data[27].name; 26 | } ) ) ); 27 | - name: debug 28 | run: echo ${{ needs.check-version.outputs.latest-tag }} 29 | pull-file: 30 | name: Update CImg.h 31 | runs-on: ubuntu-latest 32 | needs: check-version 33 | if: ${{ needs.check-version.outputs.latest-tag != '' }} 34 | steps: 35 | - uses: actions/checkout@v4 36 | name: Checkout this repository 37 | - name: Download new CImg version 38 | uses: carlosperate/download-file-action@v2 39 | with: 40 | file-url: 'https://github.com/GreycLab/CImg/raw/${{ needs.check-version.outputs.latest-tag }}/CImg.h' 41 | location: 'src' 42 | - name: Commit new CImg version 43 | run: | 44 | git config user.name 'GitHub Actions' 45 | git config user.email 'actions@github.com' 46 | git commit -am "Update CImg to ${{ needs.check-version.outputs.latest-tag }}" 47 | git push 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ###C++### 2 | 3 | # Prerequisites 4 | *.d 5 | 6 | # Compiled Object files 7 | *.slo 8 | *.lo 9 | *.o 10 | *.obj 11 | 12 | # Precompiled Headers 13 | *.gch 14 | *.pch 15 | 16 | # Compiled Dynamic libraries 17 | *.so 18 | *.dylib 19 | *.dll 20 | 21 | # Fortran module files 22 | *.mod 23 | *.smod 24 | 25 | # Compiled Static libraries 26 | *.lai 27 | *.la 28 | *.a 29 | *.lib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | tiv 36 | 37 | 38 | ###Visual Studio### 39 | 40 | .vs/ 41 | *.pdb 42 | *.idb 43 | *.ilk 44 | x64/ 45 | *.sln 46 | *.vcxproj 47 | *.vcxproj.* 48 | 49 | 50 | ###Visual Studio Code### 51 | .vscode/ 52 | 53 | 54 | ###Java### 55 | 56 | # Compiled class file 57 | *.class 58 | 59 | # Log file 60 | *.log 61 | 62 | # BlueJ files 63 | *.ctxt 64 | 65 | # Mobile Tools for Java (J2ME) 66 | .mtj.tmp/ 67 | 68 | # Package Files # 69 | *.jar 70 | *.war 71 | *.ear 72 | *.zip 73 | *.tar.gz 74 | *.rar 75 | 76 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 77 | hs_err_pid* 78 | 79 | # Intellij 80 | .idea/ 81 | *.iml 82 | 83 | ###Linux### 84 | 85 | *~ 86 | 87 | # KDE directory preferences 88 | .directory 89 | 90 | 91 | ###OSX### 92 | 93 | .DS_Store 94 | .AppleDouble 95 | .LSOverride 96 | 97 | # Icon must end with two \r 98 | Icon 99 | 100 | # Thumbnails 101 | ._* 102 | 103 | # Files that might appear on external disk 104 | .Spotlight-V100 105 | .Trashes 106 | 107 | # Directories potentially created on remote AFP share 108 | .AppleDB 109 | .AppleDesktop 110 | Network Trash Folder 111 | Temporary Items 112 | .apdisk 113 | 114 | 115 | ###Snap### 116 | 117 | parts/ 118 | prime/ 119 | snap/ 120 | stage/ 121 | *.snap 122 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Stefan Haustein and Aaron Liu 2 | 3 | This program is free software: you may copy, redistribute and/or modify it 4 | under the terms of (at your option) either the Apache License, Version 2.0, 5 | or the GNU General Public License as published by the Free Software Foundation, 6 | version 3, or any later version. 7 | 8 | This file is distributed in the hope that it will be useful, but 9 | WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 11 | 12 | For details, see either 13 | for the Apache License, Version 2.0, 14 | or for the GNU GPL, version 3. 15 | 16 | Note that the code contained in this package contains the CImg library, 17 | which is licensed under either [CeCILL 2.0](https://spdx.org/licenses/CECILL-2.0.html) 18 | (close to GPL and compatible with it) or [CeCILL-C](https://spdx.org/licenses/CECILL-C) 19 | (close to LGPL and compatible with Apache). 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terminal Image Viewer (tiv) 2 | 3 | Small C++ program to display images in a (modern) terminal using RGB ANSI codes and unicode block graphic characters. 4 | 5 | There are various similar tools (such as `timg`) that use the unicode half block character to display two 24bit pixels per character cell. This program enhances the resolution by mapping 4x8 pixel cells to different unicode characters, using the following algorithm for each 4x8 pixel cell of the (potentially downscaled) image: 6 | 7 | 1. Find the color channel (R, G or B) that has the biggest range of values for the current cell 8 | 2. Split this range in the middle and create a corresponding bitmap for the cell 9 | 3. Compare the bitmap to the assumed bitmaps for various unicode block graphics characters 10 | 4. Re-calculate the foreground and background colors for the chosen character. 11 | 12 | See the difference by disabling this optimization using the `-0` option. Or just take a look at the comparison image at the end of this text. 13 | 14 | ## Usage 15 | 16 | ```sh 17 | tiv [options] [...] 18 | ``` 19 | 20 | The shell will expand wildcards. By default, thumbnails and file names will be displayed if more than one image is provided. For a list of options, run the command without any parameters or with `--help`. 21 | 22 | ## News 23 | 24 | - 2024-03-20: Added a section on how to use the API. 25 | - 2024-02-01: We are currently working on splitting the source code into dependency-free library files and a client that uses CImg. 26 | - 2023-09-29: Today marks the 40th anniversary of the GNU project. If you haven't learned the news concerning it and Stallman, please do. 27 | Support for MSVC has been added and the repository is now under an Apache 2.0 or GPL3 dual license. CI building for each release will hopefully be setup soon. The main program has also adopted a mostly Google code-style because I (aaron) think it simply makes sense. 28 | `SPDX-License-Identifier: Apache-2.0 OR GPL-3.0-or-later` 29 | - 2021-05-22: We now support Apple Clang, thanks to the C++ filesystem library being no longer experimental. Issue forms have also been added to the GitHub repository. 30 | - 2020-10-22: The Java version is now **deprecated**. Development has long shifted to the C++ version since that was created, and the last meaningful update to it was in 2016. 31 | 32 | ## Installation 33 | 34 | > [!IMPORTANT] 35 | > All installation methods require installing ImageMagick, a required dependency. Most package managers should install it automatically. 36 | 37 | ### All platforms: Build from source 38 | 39 | Our makefile currently only supports `g++`. It should be possible to compile `tiv` manually using any of your favorite compilers that support C++17 and Unix headers (`ioctl.h` and `sysexits.h`, specifically) or `windows.h`. PRs are welcome. 40 | 41 | ```sh 42 | git clone https://github.com/stefanhaustein/TerminalImageViewer.git 43 | cd TerminalImageViewer/src 44 | make 45 | 46 | # To move the tiv binary into your PATH (hopefully), also do 47 | sudo make install 48 | ``` 49 | 50 | Please don't forget to install ImageMagick... On Debian based Linux via `sudo apt install imagemagick` and 51 | on MacOS via `brew install imagemagick`. 52 | 53 | ### Mac: Homebrew 54 | 55 | ```sh 56 | brew install tiv 57 | ``` 58 | 59 | As the original Apple Shell only supports 256 color mode (-256) and there seems to be some extra 60 | line spacing, distorting the image, we also recommend installing iTerm2: 61 | 62 | ``` 63 | brew install --cask iterm2 64 | ``` 65 | 66 | ### Third-Party Packages 67 | 68 | - @megamaced has created [an RPM for SUSE](https://build.opensuse.org/package/show/home:megamaced/terminalimageviewer) 69 | - @bperel has created [a Docker image](https://hub.docker.com/r/bperel/terminalimageviewer) 70 | 71 | 72 | ## Common problems / Troubleshooting 73 | 74 | - Errors such as "unrecognized file format"? Make sure ImageMagic is installed. 75 | - On some linux platforms, an extra flag seems to be required: `make LDLIBS=-lstdc++fs` (but it also breaks MacOs), see 76 | - If you see strange horizontal lines, the characters don't fully fill the character cell. Remove additional line spacing in your terminal app 77 | - Wrong colors? Try -256 to use a 256 color palette instead of 24 bit colors 78 | - Strange characters? Try -0 or install an use full unicode font (e.g. inconsolata or firacode) 79 | 80 | ## Using the TIV API 81 | 82 | Tiv can be used as an API. So if you always wanted to run your favorite FPS in a shell, this is the opportunity. 83 | 84 | All the code useful as a library is isolated in [tiv_lib.h](https://github.com/stefanhaustein/TerminalImageViewer/blob/master/src/tiv_lib.h) 85 | and [tiv_lib.cc](https://github.com/stefanhaustein/TerminalImageViewer/blob/master/src/tiv_lib.cc). 86 | 87 | The main entry point is 88 | 89 | ```cpp 90 | CharData findCharData(GetPixelFunction get_pixel, int x0, int y0, const int &flags) 91 | ``` 92 | 93 | The call takes a std::Function that allows the TIV code to request pixels from your framebuffer. 94 | 95 | From this framebuffer, the call will query pixels for a 4x8 pixel rectangle, where x0 and y0 96 | define the top left corner. The call searches the best unicode graphics character and colors to approximate this 97 | cell of the image, and returns these in a CharData struct. 98 | 99 | ## Contributions 100 | 101 | - 2019-03-26: Exciting week: @cabelo has fixed output redirection, @boretom has added cross-compilation support to the build file and @AlanDeSmet has fixed tall thumbnails and greyscale images. 102 | - 2020-07-05: @cxwx has fixed homebrew support. 103 | 104 | I am happy to accept useful contributions under the Apache 2.0 license, but... 105 | 106 | - Before investing in larger contributions, please use an issue to discuss this 107 | - Pull requests should be as "atomic" as possible. I won't accept any pull request doing multiple things at once. 108 | - This program currently only depends on CImg and ImageMagick as image processing libraries and I'd prefer to keep it that way. 109 | - Support for additional platforms, CPUs or similar will require somebody who is happy to help with maintenance, in particular if I don't have access to it. 110 | 111 | ## Examples 112 | 113 | Most examples were shot with the Java version of this program, which should have equivalent output but slower by millenia in CPU years. 114 | 115 | ![Examples](https://i.imgur.com/yWRZ3yk.png) 116 | 117 | If multiple images match the filename spec, thumbnails are shown. 118 | 119 | ![Thumbnails](https://i.imgur.com/PTYgSqz.png) 120 | 121 | For the example below, the top image was generated with the character optimization disabled via the `-0` option. 122 | 123 | ![Comparison](https://i.imgur.com/OzdCeh6.png) 124 | 125 | ## Licensing 126 | 127 | You are free to use this code under either the GPL (3 or later) or version 2.0 of the Apache license. We include the CImg library, which 128 | is licensed under either [CeCILL 2.0](https://spdx.org/licenses/CECILL-2.0.html) (close to GPL and compatible with it) or [CeCILL-C] (https://spdx.org/licenses/CECILL-C) (close to LGPL and compatible with Apache). 129 | -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: tiv 2 | version: "v1.1.1" 3 | summary: Terminal Image Viewer 4 | description: | 5 | tiv is a small C++ program to display images in a (modern) terminal using 6 | RGB ANSI codes and unicode block graphic characters. 7 | 8 | environment: 9 | MAGICK_CONFIGURE_PATH: $SNAP/etc/ImageMagick-7 10 | 11 | apps: 12 | tiv: 13 | command: usr/bin/tiv 14 | plugs: [home, removable-media] 15 | 16 | parts: 17 | tiv: 18 | plugin: make 19 | source-type: tar 20 | source: https://github.com/stefanhaustein/TerminalImageViewer/archive/v1.1.1.tar.gz 21 | source-subdir: src/main/cpp 22 | build-packages: 23 | - g++ 24 | - make 25 | - imagemagick 26 | override-build: | 27 | snapcraftctl build 28 | mkdir -p $SNAPCRAFT_PART_INSTALL/usr/bin/ 29 | install $SNAPCRAFT_PART_BUILD/tiv $SNAPCRAFT_PART_INSTALL/usr/bin/ 30 | imagemagick: 31 | plugin: autotools 32 | source: https://www.imagemagick.org/download/releases/ImageMagick-7.1.0-22.tar.xz 33 | source-type: tar 34 | configflags: 35 | - --enable-hdri=yes 36 | - --enable-shared=yes 37 | - --enable-static=yes 38 | - --with-autotrace=yes 39 | - --with-fpx=no 40 | - --with-gnu-ld=yes 41 | - --with-gslib=yes 42 | - --with-modules=no 43 | - --with-quantum-depth=32 44 | - --with-rsvg=yes 45 | build-packages: 46 | - autoconf 47 | - build-essential 48 | - fftw-dev 49 | - libautotrace-dev 50 | - libbz2-dev 51 | - libdjvulibre-dev 52 | - libfftw3-dev 53 | - libfontconfig1-dev 54 | - libfreetype6-dev 55 | - libgs-dev 56 | - libgvc6 57 | - libjbig-dev 58 | - libjpeg-dev 59 | - liblcms2-dev 60 | - liblqr-1-0-dev 61 | - libltdl-dev 62 | - libmagick++-dev 63 | - libopenexr-dev 64 | - libopenjp2-7-dev 65 | - libpango1.0-dev 66 | - libperl-dev 67 | - libpng12-dev 68 | - librsvg2-dev 69 | - libtiff5-dev 70 | - libwebp-dev 71 | - libwmf-dev 72 | - libx11-dev 73 | - lzma-dev 74 | - ocl-icd-opencl-dev 75 | - perlmagick 76 | - zlib1g-dev 77 | stage-packages: 78 | - libbz2-1.0 79 | - libfftw3-double3 80 | - libfontconfig1 81 | - libfreetype6 82 | - libgomp1 83 | - libjbig0 84 | - libjpeg8 85 | - liblcms2-2 86 | - liblqr-1-0 87 | - libltdl7 88 | - liblzma5 89 | - libpng12-0 90 | - libtiff5 91 | - libx11-6 92 | - libxext6 93 | - libxml2 94 | - zlib1g 95 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | PROGNAME = tiv 2 | 3 | OBJECTS = tiv.o tiv_lib.o 4 | 5 | CXX ?= g++ 6 | CXXFLAGS ?= -O2 -fpermissive 7 | INSTALL ?= install 8 | INSTALL_PROGRAM ?= $(INSTALL) -D 9 | 10 | # https://www.gnu.org/prep/standards/html_node/Directory-Variables.html#Directory-Variables 11 | prefix ?= /usr/local 12 | exec_prefix ?= $(prefix) 13 | bindir ?= $(exec_prefix)/bin 14 | 15 | override CXXFLAGS += -std=c++17 -Wall -fexceptions 16 | override LDFLAGS += -pthread 17 | override LDLIBS += -lpng 18 | 19 | all: $(PROGNAME) 20 | 21 | tiv_lib.o: tiv_lib.h 22 | 23 | tiv.o: CImg.h tiv_lib.h 24 | 25 | $(PROGNAME): $(OBJECTS) 26 | $(CXX) $(LDFLAGS) $^ -o $@ $(LOADLIBES) $(LDLIBS) 27 | 28 | install: all 29 | $(INSTALL_PROGRAM) $(PROGNAME) $(DESTDIR)$(bindir)/$(PROGNAME) 30 | 31 | clean: 32 | $(RM) -f $(PROGNAME) *.o 33 | 34 | .PHONY: all install clean 35 | -------------------------------------------------------------------------------- /src/java/TerminalImageViewer.java: -------------------------------------------------------------------------------- 1 | import java.awt.Graphics2D; 2 | import java.awt.image.BufferedImage; 3 | import java.io.BufferedReader; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.InputStreamReader; 7 | import java.net.URL; 8 | import java.util.Arrays; 9 | import java.util.regex.Matcher; 10 | 11 | import javax.imageio.ImageIO; 12 | 13 | /** 14 | * Simple program to print images to the shell using 24 bit ANSI color codes and Unicode block graphics characters. 15 | * 16 | * License: Apache 2.0 17 | * @author Stefan Haustein 18 | */ 19 | public class TerminalImageViewer { 20 | 21 | static boolean grayscale = false; 22 | static int mode = Ansi.MODE_24BIT; 23 | static boolean html = false; 24 | 25 | 26 | /** 27 | * Main method, handles command line arguments and loads and scales images. 28 | */ 29 | public static void main(String[] args) throws IOException { 30 | if (args.length == 0) { 31 | System.out.println( 32 | "Image file name required.\n\n" + 33 | "TerminalImageViewer Java\n" + 34 | " - Use -w and -h to set the maximum width and height in characters (defaults: 80, 24).\n" + 35 | " - Use -256 for 256 color mode, -grayscale for grayscale and -stdin to obtain file names from stdin.\n" + 36 | " - When multiple files are supplied, -c sets the number of images per row (default: 4)." + 37 | "NOTE: This version of TerminalImageViewer is deprecated. Please use the C++ version instead.\n"); 38 | return; 39 | } 40 | 41 | int start = 0; 42 | int maxWidth = 80; 43 | int maxHeight = 24; 44 | int columns = 4; 45 | boolean stdin = false; 46 | while (start < args.length && args[start].startsWith("-")) { 47 | String option = args[start]; 48 | if (option.equals("-w") && args.length > start + 1) { 49 | maxWidth = Integer.parseInt(args[++start]); 50 | } else if (option.equals("-h") && args.length > start + 1) { 51 | maxHeight = Integer.parseInt(args[++start]); 52 | } else if (option.equals("-c") && args.length > start + 1) { 53 | columns = Integer.parseInt(args[++start]); 54 | } else if (option.equals("-256")) { 55 | mode = (mode & ~Ansi.MODE_24BIT) | Ansi.MODE_256; 56 | } else if (option.equals("-grayscale")) { 57 | grayscale = true; 58 | } else if (option.equals("-html")) { 59 | html = true; 60 | } else if (option.equals("-stdin")) { 61 | stdin = true; 62 | } 63 | start++; 64 | } 65 | 66 | maxWidth *= 4; 67 | maxHeight *= 8; 68 | 69 | if (stdin) { 70 | BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 71 | while (true) { 72 | String name = reader.readLine(); 73 | if (name == null || name.isEmpty()) { 74 | break; 75 | } 76 | convert(name, maxWidth, maxHeight); 77 | } 78 | } else if (start == args.length - 1 && (isUrl(args[start]) || !new File(args[start]).isDirectory())) { 79 | convert(args[start], maxWidth, maxHeight); 80 | } else { 81 | // Directory-style rendering. 82 | int index = 0; 83 | int cw = (maxWidth - 2 * (columns - 1) * 4) / (4 * columns); 84 | int tw = cw * 4; 85 | 86 | while (index < args.length) { 87 | BufferedImage image = new BufferedImage(tw * columns + 24, tw, grayscale ? BufferedImage.TYPE_BYTE_GRAY : BufferedImage.TYPE_INT_RGB); 88 | Graphics2D graphics = image.createGraphics(); 89 | int count = 0; 90 | StringBuilder sb = new StringBuilder(); 91 | while (index < args.length && count < columns) { 92 | String name = args[index++]; 93 | try { 94 | BufferedImage original = loadImage(name); 95 | int cut = name.lastIndexOf('/'); 96 | sb.append(name.substring(cut + 1)); 97 | int th = original.getHeight() * tw / original.getWidth(); 98 | graphics.drawImage(original, count * (tw + 8), (tw - th) / 2, tw, th, null); 99 | count++; 100 | int sl = count * (cw + 2); 101 | while (sb.length() < sl - 2) { 102 | sb.append(' '); 103 | } 104 | sb.setLength(sl - 2); 105 | sb.append(" "); 106 | } catch (Exception e) { 107 | // Probably no image; ignore. 108 | } 109 | } 110 | dump(image, mode); 111 | System.out.println(sb.toString()); 112 | System.out.println(); 113 | } 114 | } 115 | } 116 | 117 | static boolean isUrl(String name) { 118 | return name.startsWith("http://") || name.startsWith("https://"); 119 | } 120 | 121 | static void convert(String name, int maxWidth, int maxHeight) throws IOException { 122 | BufferedImage original = loadImage(name); 123 | 124 | float originalWidth = original.getWidth(); 125 | float originalHeight = original.getHeight(); 126 | float scale = Math.min(maxWidth / originalWidth, maxHeight / originalHeight); 127 | int height = (int) (originalHeight * scale); 128 | int width = (int) (originalWidth * scale); 129 | 130 | if (originalWidth == width && !grayscale) { 131 | dump(original, mode); 132 | } else { 133 | BufferedImage image = new BufferedImage(width, height, grayscale ? BufferedImage.TYPE_BYTE_GRAY : BufferedImage.TYPE_INT_RGB); 134 | Graphics2D graphics = image.createGraphics(); 135 | graphics.drawImage(original, 0, 0, width, height, null); 136 | dump(image, mode); 137 | } 138 | } 139 | 140 | static BufferedImage loadImage(String name) throws IOException { 141 | if (isUrl(name)) { 142 | URL url = new URL(name); 143 | return ImageIO.read(url); 144 | } 145 | return ImageIO.read(new File(name)); 146 | } 147 | 148 | static void dump(BufferedImage image, int mode) { 149 | int w = image.getWidth(); 150 | ImageData imageData = new ImageData(w, image.getHeight()); 151 | byte[] data = imageData.data; 152 | int[] rgbArray = new int[w]; 153 | for (int y = 0; y < image.getHeight(); y++) { 154 | image.getRGB(0, y, image.getWidth(), 1, rgbArray, 0, w); 155 | int pos = y * w * 4; 156 | for (int x = 0; x < w; x++) { 157 | int rgb = rgbArray[x]; 158 | data[pos++] = (byte) (rgb >> 16); 159 | data[pos++] = (byte) (rgb >> 8); 160 | data[pos++] = (byte) rgb; 161 | pos++; 162 | } 163 | } 164 | System.out.print(imageData.dump(mode)); 165 | } 166 | 167 | 168 | 169 | /** 170 | * ANSI control code helpers 171 | */ 172 | static class Ansi { 173 | public static final String RESET = "\u001b[0m"; 174 | public static int FG = 1; 175 | public static int BG = 2; 176 | public static int MODE_256 = 4; 177 | public static int MODE_24BIT = 8; 178 | 179 | public static final int[] COLOR_STEPS = {0, 0x5f, 0x87, 0xaf, 0xd7, 0xff}; 180 | public static final int[] GRAYSCALE = {0x08, 0x12, 0x1c, 0x26, 0x30, 0x3a, 0x44, 0x4e, 0x58, 0x62, 0x6c, 0x76, 181 | 0x80, 0x8a, 0x94, 0x9e, 0xa8, 0xb2, 0xbc, 0xc6, 0xd0, 0xda, 0xe4, 0xee}; 182 | 183 | static int bestIndex(int v, int[] options) { 184 | int index = Arrays.binarySearch(options, v); 185 | if (index < 0) { 186 | index = -index - 1; 187 | // need to check [index] and [index - 1] 188 | if (index == options.length) { 189 | index = options.length - 1; 190 | } else if (index > 0) { 191 | int val0 = options[index - 1]; 192 | int val1 = options[index]; 193 | if (v - val0 < val1 - v) { 194 | index = index - 1; 195 | } 196 | } 197 | } 198 | return index; 199 | } 200 | 201 | static int sqr(int i) { 202 | return i * i; 203 | } 204 | 205 | public static int clamp(int value, int min, int max) { 206 | return Math.min(Math.max(value, min), max); 207 | } 208 | 209 | public static String color(int flags, int r, int g, int b) { 210 | r = clamp(r, 0, 255); 211 | g = clamp(g, 0, 255); 212 | b = clamp(b, 0, 255); 213 | 214 | boolean bg = (flags & BG) != 0; 215 | 216 | if ((flags & MODE_256) == 0) { 217 | return (bg ? "\u001b[48;2;" : "\u001b[38;2;") + r + ";" + g + ";" + b + "m"; 218 | } 219 | int rIdx = bestIndex(r, COLOR_STEPS); 220 | int gIdx = bestIndex(g, COLOR_STEPS); 221 | int bIdx = bestIndex(b, COLOR_STEPS); 222 | 223 | int rQ = COLOR_STEPS[rIdx]; 224 | int gQ = COLOR_STEPS[gIdx]; 225 | int bQ = COLOR_STEPS[bIdx]; 226 | 227 | int gray = Math.round(r * 0.2989f + g * 0.5870f + b * 0.1140f); 228 | 229 | int grayIdx = bestIndex(gray, GRAYSCALE); 230 | int grayQ = GRAYSCALE[grayIdx]; 231 | 232 | int colorIndex; 233 | if (0.3 * sqr(rQ-r) + 0.59 * sqr(gQ-g) + 0.11 *sqr(bQ-b) < 234 | 0.3 * sqr(grayQ-r) + 0.59 * sqr(grayQ-g) + 0.11 * sqr(grayQ-b)) { 235 | colorIndex = 16 + 36 * rIdx + 6 * gIdx + bIdx; 236 | } else { 237 | colorIndex = 232 + grayIdx; // 1..24 -> 232..255 238 | } 239 | return (bg ? "\u001B[48;5;" : "\u001B[38;5;") + colorIndex + "m"; 240 | } 241 | } 242 | 243 | /** 244 | * Converts 4x8 RGB pixel to a unicode character and a foreground and background color: 245 | * Uses a variation of the median cut algorithm to determine a two-color palette for the 246 | * character, then creates a corresponding bitmap for the partial image covered by the 247 | * character and finds the best match in the character bitmap table. 248 | */ 249 | static class BlockChar { 250 | 251 | /** 252 | * Assumed bitmaps of the supported characters 253 | */ 254 | static int[] BITMAPS = new int[] { 255 | 0x00000000, '\u00a0', 256 | 257 | // Block graphics 258 | 259 | // 0xffff0000, '\u2580', // upper 1/2; redundant with inverse lower 1/2 260 | 261 | 0x0000000f, '\u2581', // lower 1/8 262 | 0x000000ff, '\u2582', // lower 1/4 263 | 0x00000fff, '\u2583', 264 | 0x0000ffff, '\u2584', // lower 1/2 265 | 0x000fffff, '\u2585', 266 | 0x00ffffff, '\u2586', // lower 3/4 267 | 0x0fffffff, '\u2587', 268 | // 0xffffffff, '\u2588', // full; redundant with inverse space 269 | 270 | 0xeeeeeeee, '\u258a', // left 3/4 271 | 0xcccccccc, '\u258c', // left 1/2 272 | 0x88888888, '\u258e', // left 1/4 273 | 274 | 0x0000cccc, '\u2596', // quadrant lower left 275 | 0x00003333, '\u2597', // quadrant lower right 276 | 0xcccc0000, '\u2598', // quadrant upper left 277 | // 0xccccffff, '\u2599', // 3/4 redundant with inverse 1/4 278 | 0xcccc3333, '\u259a', // diagonal 1/2 279 | // 0xffffcccc, '\u259b', // 3/4 redundant 280 | // 0xffff3333, '\u259c', // 3/4 redundant 281 | 0x33330000, '\u259d', // quadrant upper right 282 | // 0x3333cccc, '\u259e', // 3/4 redundant 283 | // 0x3333ffff, '\u259f', // 3/4 redundant 284 | 285 | // Line drawing subset: no double lines, no complex light lines 286 | // Simple light lines duplicated because there is no center pixel int the 4x8 matrix 287 | 288 | 0x000ff000, '\u2501', // Heavy horizontal 289 | 0x66666666, '\u2503', // Heavy vertical 290 | 291 | 0x00077666, '\u250f', // Heavy down and right 292 | 0x000ee666, '\u2513', // Heavy down and left 293 | 0x66677000, '\u2517', // Heavy up and right 294 | 0x666ee000, '\u251b', // Heavy up and left 295 | 296 | 0x66677666, '\u2523', // Heavy vertical and right 297 | 0x666ee666, '\u252b', // Heavy vertical and left 298 | 0x000ff666, '\u2533', // Heavy down and horizontal 299 | 0x666ff000, '\u253b', // Heavy up and horizontal 300 | 0x666ff666, '\u254b', // Heavy cross 301 | 302 | 0x000cc000, '\u2578', // Bold horizontal left 303 | 0x00066000, '\u2579', // Bold horizontal up 304 | 0x00033000, '\u257a', // Bold horizontal right 305 | 0x00066000, '\u257b', // Bold horizontal down 306 | 307 | 0x06600660, '\u254f', // Heavy double dash vertical 308 | 309 | 0x000f0000, '\u2500', // Light horizontal 310 | 0x0000f000, '\u2500', // 311 | 0x44444444, '\u2502', // Light vertical 312 | 0x22222222, '\u2502', 313 | 314 | 0x000e0000, '\u2574', // light left 315 | 0x0000e000, '\u2574', // light left 316 | 0x44440000, '\u2575', // light up 317 | 0x22220000, '\u2575', // light up 318 | 0x00030000, '\u2576', // light right 319 | 0x00003000, '\u2576', // light right 320 | 0x00004444, '\u2575', // light down 321 | 0x00002222, '\u2575', // light down 322 | 323 | // Misc technical 324 | 325 | 0x44444444, '\u23a2', // [ extension 326 | 0x22222222, '\u23a5', // ] extension 327 | 328 | //12345678 329 | 0x0f000000, '\u23ba', // Horizontal scanline 1 330 | 0x00f00000, '\u23bb', // Horizontal scanline 3 331 | 0x00000f00, '\u23bc', // Horizontal scanline 7 332 | 0x000000f0, '\u23bd', // Horizontal scanline 9 333 | 334 | // Geometrical shapes. Tricky because some of them are too wide. 335 | 336 | // 0x00ffff00, '\u25fe', // Black medium small square 337 | 0x00066000, '\u25aa', // Black small square 338 | 339 | /* 340 | 0x11224488, '\u2571', // diagonals 341 | 0x88442211, '\u2572', 342 | 0x99666699, '\u2573', 343 | 344 | 0x000137f0, '\u25e2', // Triangles 345 | 0x0008cef0, '\u25e3', 346 | 0x000fec80, '\u25e4', 347 | 0x000f7310, '\u25e5' 348 | */ 349 | }; 350 | 351 | /** Minimum value for each color channel. */ 352 | int[] min = new int[3]; 353 | 354 | /** Maximum value for each color channel. */ 355 | int[] max = new int[3]; 356 | 357 | /** Red, green and blue components of the selected background color. */ 358 | int[] bgColor = new int[3]; 359 | 360 | /** Red, green and blue components of the selected background color. */ 361 | int[] fgColor = new int[3]; 362 | 363 | /** The selected character. */ 364 | char character; 365 | 366 | /** 367 | * Converts a set of pixels to a unicode character and a background and foreground color. 368 | * data contains the rgba values, p0 is the start point in data and scanWidth the number 369 | * of bytes in each row of data. 370 | */ 371 | void load(byte[] data, int p0, int scanWidth) { 372 | Arrays.fill(min, 255); 373 | Arrays.fill(max, 0); 374 | Arrays.fill(bgColor, 0); 375 | Arrays.fill(fgColor, 0); 376 | 377 | // Determine the minimum and maximum value for each color channel 378 | int pos = p0; 379 | for (int y = 0; y < 8; y++) { 380 | for (int x = 0; x < 4; x++) { 381 | for (int i = 0; i < 3; i++) { 382 | int d = data[pos++] & 255; 383 | min[i] = Math.min(min[i], d); 384 | max[i] = Math.max(max[i], d); 385 | } 386 | pos++; // Alpha 387 | } 388 | pos += scanWidth - 16; 389 | } 390 | 391 | // Determine the color channel with the greatest range. 392 | int splitIndex = 0; 393 | int bestSplit = 0; 394 | for (int i = 0; i < 3; i++) { 395 | if (max[i] - min[i] > bestSplit) { 396 | bestSplit = max[i] - min[i]; 397 | splitIndex = i; 398 | } 399 | } 400 | // We just split at the middle of the interval instead of computing the median. 401 | int splitValue = min[splitIndex] + bestSplit / 2; 402 | 403 | // Compute a bitmap using the given split and sum the color values for both buckets. 404 | int bits = 0; 405 | int fgCount = 0; 406 | int bgCount = 0; 407 | 408 | pos = p0; 409 | for (int y = 0; y < 8; y++) { 410 | for (int x = 0; x < 4; x++) { 411 | bits = bits << 1; 412 | int[] avg; 413 | if ((data[pos + splitIndex] & 255) > splitValue) { 414 | avg = fgColor; 415 | bits |= 1; 416 | fgCount++; 417 | } else { 418 | avg = bgColor; 419 | bgCount++; 420 | } 421 | for (int i = 0; i < 3; i++) { 422 | avg[i] += data[pos++] & 255; 423 | } 424 | pos++; // Alpha 425 | } 426 | pos += scanWidth - 16; 427 | } 428 | 429 | // Calculate the average color value for each bucket 430 | for (int i = 0; i < 3; i++) { 431 | if (bgCount != 0) { 432 | bgColor[i] /= bgCount; 433 | } 434 | if (fgCount != 0) { 435 | fgColor[i] /= fgCount; 436 | } 437 | } 438 | 439 | // Find the best bitmap match by counting the bits that don't match, including 440 | // the inverted bitmaps. 441 | int bestDiff = Integer.MAX_VALUE; 442 | boolean invert = false; 443 | for (int i = 0; i < BITMAPS.length; i += 2) { 444 | int diff = Integer.bitCount(BITMAPS[i] ^ bits); 445 | if (diff < bestDiff) { 446 | character = (char) BITMAPS[i + 1]; 447 | bestDiff = diff; 448 | invert = false; 449 | } 450 | diff = Integer.bitCount((~BITMAPS[i]) ^ bits); 451 | if (diff < bestDiff) { 452 | character = (char) BITMAPS[i + 1]; 453 | bestDiff = diff; 454 | invert = true; 455 | } 456 | } 457 | 458 | // If the match is quite bad, use a shade image instead. 459 | if (bestDiff > 10) { 460 | invert = false; 461 | character = " \u2591\u2592\u2593\u2588".charAt(Math.min(4, fgCount * 5 / 32)); 462 | } 463 | 464 | // If we use an inverted character, we need to swap the colors. 465 | if (invert) { 466 | int[] tmp = bgColor; 467 | bgColor = fgColor; 468 | fgColor = tmp; 469 | } 470 | } 471 | } 472 | 473 | /** 474 | * Roughly modeled after the corresponding HTML 5 class. 475 | */ 476 | static class ImageData { 477 | public final int width; 478 | public final int height; 479 | public final byte[] data; 480 | 481 | public ImageData(int width, int height) { 482 | this.width = width; 483 | this.height = height; 484 | this.data = new byte[width * height * 4]; 485 | } 486 | 487 | public String hex6(int r, int g, int b) { 488 | return Integer.toHexString((1 << 24) | ((r & 255) << 16) | ((g & 255) << 8) | (b & 255)).substring(1); 489 | } 490 | /** 491 | * Convert the image to an Ansi control character string setting the colors 492 | */ 493 | public String dump(int mode) { 494 | StringBuilder sb = new StringBuilder(); 495 | BlockChar blockChar = new BlockChar(); 496 | 497 | for (int y = 0; y < height - 7; y += 8) { 498 | int pos = y * width * 4; 499 | if (html) { 500 | String last = ""; 501 | for (int x = 0; x < width - 3; x += 4) { 502 | blockChar.load(data, pos, width * 4); 503 | String fg = hex6(blockChar.fgColor[0], blockChar.fgColor[1], blockChar.fgColor[2]); 504 | String bg = hex6(blockChar.bgColor[0], blockChar.bgColor[1], blockChar.bgColor[2]); 505 | String style = "background-color:#" + bg + ";color:#" + fg; 506 | if (!style.equals(last)) { 507 | if (!last.isEmpty()) { 508 | sb.append(""); 509 | } 510 | sb.append(""); 511 | last = style; 512 | } 513 | sb.append("&#" + ((int) blockChar.character) + ";"); 514 | pos += 16; 515 | } 516 | sb.append("
\n"); 517 | } else { 518 | String lastFg = ""; 519 | String lastBg = ""; 520 | for (int x = 0; x < width - 3; x += 4) { 521 | blockChar.load(data, pos, width * 4); 522 | String fg = Ansi.color(Ansi.FG | mode, blockChar.fgColor[0], blockChar.fgColor[1], blockChar.fgColor[2]); 523 | String bg = Ansi.color(Ansi.BG | mode, blockChar.bgColor[0], blockChar.bgColor[1], blockChar.bgColor[2]); 524 | if (!fg.equals(lastFg)) { 525 | sb.append(fg); 526 | lastFg = fg; 527 | } 528 | if (!bg.equals(lastBg)) { 529 | sb.append(bg); 530 | lastBg = bg; 531 | } 532 | sb.append(blockChar.character); 533 | pos += 16; 534 | } 535 | sb.append(Ansi.RESET).append("\n"); 536 | } 537 | } 538 | return sb.toString(); 539 | } 540 | } 541 | } 542 | -------------------------------------------------------------------------------- /src/tiv.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2023, Stefan Haustein, Aaron Liu 3 | * 4 | * This file is free software: you may copy, redistribute and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. 8 | * 9 | * This file is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | * Alternatively, you may copy, redistribute and/or modify this file under 18 | * the terms of the Apache License, version 2.0: 19 | * 20 | * Licensed under the Apache License, Version 2.0 (the "License"); 21 | * you may not use this file except in compliance with the License. 22 | * You may obtain a copy of the License at 23 | * 24 | * https://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software 27 | * distributed under the License is distributed on an "AS IS" BASIS, 28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | * See the License for the specific language governing permissions and 30 | * limitations under the License. 31 | */ 32 | 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | 42 | #include "tiv_lib.h" 43 | 44 | // This #define tells CImg that we use the library without any display options 45 | // -- just for loading images. 46 | #define cimg_display 0 47 | #define cimg_use_png 48 | #include "CImg.h" 49 | 50 | #ifdef _POSIX_VERSION 51 | // Console output size detection 52 | #include 53 | // Error explanation, for some reason 54 | #include 55 | #endif 56 | 57 | #ifdef _WIN32 58 | #include 59 | // Error explanation 60 | #include 61 | #endif 62 | 63 | // Program exit code constants compatible with sysexits.h. 64 | #define EXITCODE_OK 0 65 | #define EXITCODE_COMMAND_LINE_USAGE_ERROR 64 66 | #define EXITCODE_DATA_FORMAT_ERROR 65 67 | #define EXITCODE_NO_INPUT_ERROR 66 68 | 69 | void printTermColor(const int &flags, int r, int g, int b) { 70 | r = clamp_byte(r); 71 | g = clamp_byte(g); 72 | b = clamp_byte(b); 73 | 74 | bool bg = (flags & FLAG_BG) != 0; 75 | 76 | if ((flags & FLAG_MODE_256) == 0) { 77 | std::cout << (bg ? "\x1b[48;2;" : "\x1b[38;2;") << r << ';' << g << ';' 78 | << b << 'm'; 79 | return; 80 | } 81 | 82 | int ri = best_index(r, COLOR_STEPS, COLOR_STEP_COUNT); 83 | int gi = best_index(g, COLOR_STEPS, COLOR_STEP_COUNT); 84 | int bi = best_index(b, COLOR_STEPS, COLOR_STEP_COUNT); 85 | 86 | int rq = COLOR_STEPS[ri]; 87 | int gq = COLOR_STEPS[gi]; 88 | int bq = COLOR_STEPS[bi]; 89 | 90 | int gray = 91 | static_cast(std::round(r * 0.2989f + g * 0.5870f + b * 0.1140f)); 92 | 93 | int gri = best_index(gray, GRAYSCALE_STEPS, GRAYSCALE_STEP_COUNT); 94 | int grq = GRAYSCALE_STEPS[gri]; 95 | 96 | int color_index; 97 | if (0.3 * sqr(rq - r) + 0.59 * sqr(gq - g) + 0.11 * sqr(bq - b) < 98 | 0.3 * sqr(grq - r) + 0.59 * sqr(grq - g) + 0.11 * sqr(grq - b)) { 99 | color_index = 16 + 36 * ri + 6 * gi + bi; 100 | } else { 101 | color_index = 232 + gri; // 1..24 -> 232..255 102 | } 103 | std::cout << (bg ? "\x1B[48;5;" : "\u001B[38;5;") << color_index << "m"; 104 | } 105 | 106 | void printCodepoint(int codepoint) { 107 | if (codepoint < 128) { 108 | std::cout << static_cast(codepoint); 109 | } else if (codepoint < 0x7ff) { 110 | std::cout << static_cast(0xc0 | (codepoint >> 6)); 111 | std::cout << static_cast(0x80 | (codepoint & 0x3f)); 112 | } else if (codepoint < 0xffff) { 113 | std::cout << static_cast(0xe0 | (codepoint >> 12)); 114 | std::cout << static_cast(0x80 | ((codepoint >> 6) & 0x3f)); 115 | std::cout << static_cast(0x80 | (codepoint & 0x3f)); 116 | } else if (codepoint < 0x10ffff) { 117 | std::cout << static_cast(0xf0 | (codepoint >> 18)); 118 | std::cout << static_cast(0x80 | ((codepoint >> 12) & 0x3f)); 119 | std::cout << static_cast(0x80 | ((codepoint >> 6) & 0x3f)); 120 | std::cout << static_cast(0x80 | (codepoint & 0x3f)); 121 | } else { 122 | std::cerr << "ERROR"; 123 | } 124 | } 125 | 126 | void printImage(const cimg_library::CImg &image, 127 | const int &flags) { 128 | GetPixelFunction get_pixel = [&](int x, int y) -> unsigned long { 129 | return (((unsigned long) image(x, y, 0, 0)) << 16) 130 | | (((unsigned long) image(x, y, 0, 1)) << 8) 131 | | (((unsigned long) image(x, y, 0, 2))); 132 | }; 133 | 134 | CharData lastCharData; 135 | for (int y = 0; y <= image.height() - 8; y += 8) { 136 | for (int x = 0; x <= image.width() - 4; x += 4) { 137 | CharData charData = 138 | flags & FLAG_NOOPT 139 | ? createCharData(get_pixel, x, y, 0x2584, 0x0000ffff) 140 | : findCharData(get_pixel, x, y, flags); 141 | if (x == 0 || charData.bgColor != lastCharData.bgColor) 142 | printTermColor(flags | FLAG_BG, charData.bgColor[0], 143 | charData.bgColor[1], charData.bgColor[2]); 144 | if (x == 0 || charData.fgColor != lastCharData.fgColor) 145 | printTermColor(flags | FLAG_FG, charData.fgColor[0], 146 | charData.fgColor[1], charData.fgColor[2]); 147 | printCodepoint(charData.codePoint); 148 | lastCharData = charData; 149 | } 150 | std::cout << "\x1b[0m" << std::endl; 151 | } 152 | } 153 | 154 | struct size { 155 | size(unsigned int in_width, unsigned int in_height) 156 | : width(in_width), height(in_height) {} 157 | explicit size(cimg_library::CImg img) 158 | : width(img.width()), height(img.height()) {} 159 | unsigned int width; 160 | unsigned int height; 161 | size scaled(double scale) { return size(width * scale, height * scale); } 162 | size fitted_within(size container) { 163 | double scale = std::min(container.width / static_cast(width), 164 | container.height / static_cast(height)); 165 | return scaled(scale); 166 | } 167 | }; 168 | std::ostream &operator<<(std::ostream &stream, size sz) { 169 | stream << sz.width << "x" << sz.height; 170 | return stream; 171 | } 172 | 173 | /** 174 | * @brief Wrapper around CImg(const char*) constructor 175 | * that always returns a CImg image with 3 channels (RGB) 176 | * 177 | * @param filename The file to construct a CImg object on 178 | * @param bgColor The color to use as the background in case of a transparent image 179 | * @return cimg_library::CImg Constructed CImg RGB image 180 | */ 181 | cimg_library::CImg load_rgb_CImg(const char *const &filename, 182 | unsigned char* bgColor) { 183 | cimg_library::CImg image(filename); 184 | // Regular image, do nothing special 185 | if (image.spectrum() == 3) { 186 | if (!(bgColor[0] == 255 && bgColor[1] == 255 && bgColor[2] == 255)) { 187 | std::cerr << "Warning: Background color argument '-C' was specified, " 188 | "but only PNGs and GIFs transparencies are supported " 189 | "at the moment. Using white background instead." 190 | << std::endl; 191 | } 192 | return image; 193 | } 194 | 195 | cimg_library::CImg rgb_image( 196 | image.width(), image.height(), image.depth(), 3); 197 | 198 | if (image.spectrum() == 1) { 199 | // Greyscale. Just copy greyscale data to all channels 200 | for (unsigned int chn = 0; chn < 3; chn++) 201 | rgb_image.draw_image(0, 0, 0, chn, image); 202 | } else if (image.spectrum() == 4) { 203 | // Transparent image, fill background then draw image over 204 | for (unsigned int chn = 0; chn < 3; chn++) 205 | rgb_image.get_shared_channel(chn).fill(bgColor[chn]); 206 | rgb_image.draw_image(0, 0, image.get_shared_channels(0, 2), 207 | image.get_shared_channel(3), 1, 255); 208 | } 209 | return rgb_image; 210 | } 211 | 212 | // Implements --help 213 | void printUsage() { 214 | std::cerr << R"( 215 | Terminal Image Viewer v1.2.1 216 | usage: tiv [options] [...] 217 | -0 : No block character adjustment, always use top half block char. 218 | -2, --256 : Use 256-bit colors. Needed to display properly on macOS Terminal. 219 | -c : Number of thumbnail columns in 'dir' mode (3 by default). 220 | -d, --dir : Force 'dir' mode. Automatically selected for more than one input. 221 | -f, --full: Force 'full' mode. Automatically selected for one input. 222 | --help : Display this help text. 223 | -h : Set the maximum output height to lines. 224 | -w : Set the maximum output width to characters. 225 | -C : Use hex color (0xFFFFFF (White) by default) as background for PNG/GIF. 226 | -x : Use new Unicode Teletext/legacy characters (experimental).)" 227 | << std::endl; 228 | } 229 | 230 | enum Mode { AUTO, THUMBNAILS, FULL_SIZE }; 231 | 232 | int main(int argc, char *argv[]) { 233 | std::ios::sync_with_stdio(false); // apparently makes printing faster 234 | bool detectSize = true; 235 | 236 | // Platform-specific implementations for determining console size, better 237 | // implementations are welcome Fallback sizes when unsuccessful 238 | int maxWidth = 80; 239 | int maxHeight = 24; 240 | 241 | // Default background color (white) 242 | unsigned char bgColor[] = { 255, 255, 255 }; 243 | 244 | // Reading input 245 | char flags = 0; // bitwise representation of flags, 246 | // see https://stackoverflow.com/a/14295472 247 | Mode mode = AUTO; // either THUMBNAIL or FULL_SIZE 248 | int columns = 3; 249 | 250 | std::vector file_names; 251 | int ret = EXITCODE_OK; // The return code for the program 252 | 253 | if (argc <= 1) { 254 | printUsage(); 255 | return EXITCODE_COMMAND_LINE_USAGE_ERROR; 256 | } 257 | 258 | for (int i = 1; i < argc; i++) { 259 | std::string arg(argv[i]); 260 | if (arg == "-0") { 261 | flags |= FLAG_NOOPT; 262 | } else if (arg == "-c") { 263 | if (i < argc - 1) { 264 | columns = std::stoi(argv[++i]); 265 | } else { 266 | std::cerr << "Error: -c requires a number" << std::endl; 267 | ret = EXITCODE_COMMAND_LINE_USAGE_ERROR; 268 | } 269 | } else if (arg == "-d" || arg == "--dir") { 270 | mode = THUMBNAILS; 271 | } else if (arg == "-f" || arg == "--full") { 272 | mode = FULL_SIZE; 273 | } else if (arg == "-w") { 274 | if (i < argc - 1) { 275 | maxWidth = 4 * std::stoi(argv[++i]), detectSize = false; 276 | } else { 277 | std::cerr << "Error: -w requires a number" << std::endl; 278 | ret = EXITCODE_COMMAND_LINE_USAGE_ERROR; 279 | } 280 | } else if (arg == "-h") { 281 | if (i < argc - 1) 282 | maxHeight = 8 * std::stoi(argv[++i]), detectSize = false; 283 | else 284 | printUsage(); // people might confuse this with help 285 | } else if (arg == "--256" || arg == "-2" || arg == "-256") { 286 | flags |= FLAG_MODE_256; 287 | } else if (arg == "--help" || arg == "-help") { 288 | printUsage(); 289 | } else if (arg == "-C") { 290 | if (i < argc - 1) { 291 | unsigned long hexIn = std::strtol(argv[++i], nullptr, 16); 292 | for (unsigned int chn = 0; chn < 3; chn++) 293 | bgColor[chn] = get_channel(hexIn, chn); 294 | } else { 295 | std::cerr << "Error: -C requires an argument" << std::endl; 296 | ret = EXITCODE_COMMAND_LINE_USAGE_ERROR; 297 | } 298 | } else if (arg == "-x") { 299 | flags |= FLAG_TELETEXT; 300 | } else if (arg[0] == '-') { 301 | std::cerr << "Error: Unrecognized argument: " << arg << std::endl; 302 | ret = EXITCODE_COMMAND_LINE_USAGE_ERROR; 303 | } else { 304 | // Arguments that will be displayed 305 | if (std::filesystem::is_directory(arg)) { 306 | for (auto &p : std::filesystem::directory_iterator(arg)) 307 | if (std::filesystem::is_regular_file(p.path())) 308 | file_names.push_back(p.path().string()); 309 | } else { 310 | // Check if file can be opened, @TODO find better way 311 | std::ifstream fin(arg.c_str()); 312 | if (fin) { 313 | file_names.push_back(arg); 314 | } else { 315 | std::cerr << "Error: Cannot open '" << arg 316 | << "', permission issue?" << std::endl; 317 | ret = EXITCODE_NO_INPUT_ERROR; 318 | } 319 | } 320 | } 321 | } 322 | 323 | if (detectSize) { 324 | #ifdef _POSIX_VERSION 325 | struct winsize w; 326 | // If redirecting STDOUT to one file ( col or row == 0, or the previous 327 | // ioctl call's failed ) 328 | if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) != 0 || 329 | (w.ws_col | w.ws_row) == 0) { 330 | std::cerr << "Warning: failed to determine most reasonable size: " 331 | << strerror(errno) << ", defaulting to 20x6" << std::endl; 332 | } else { 333 | maxWidth = w.ws_col * 4; 334 | maxHeight = w.ws_row * 8; 335 | } 336 | #elif defined _WIN32 337 | CONSOLE_SCREEN_BUFFER_INFO w; 338 | if (GetConsoleScreenBufferInfo( 339 | GetStdHandle(STD_OUTPUT_HANDLE), 340 | &w)) { // just like PowerShell, but without the hyphens, hooray 341 | maxWidth = w.dwSize.X * 4; 342 | maxHeight = w.dwSize.Y * 8; 343 | } else { 344 | std::cerr << "Warning: failed to determine most reasonable size: " 345 | << std::system_category().message(GetLastError()) 346 | << ", defaulting to 80x24" << std::endl; 347 | } 348 | #else 349 | std::cerr << "Warning: failed to determine most reasonable size: " 350 | "unrecognized system, defaulting to 80x24" 351 | << std::endl; 352 | #endif 353 | } 354 | 355 | if (mode == FULL_SIZE || (mode == AUTO && file_names.size() == 1)) { 356 | for (const auto &filename : file_names) { 357 | try { 358 | cimg_library::CImg image = 359 | load_rgb_CImg(filename.c_str(), bgColor); 360 | if (image.width() > maxWidth || image.height() > maxHeight) { 361 | // scale image down to fit terminal size 362 | size new_size = 363 | size(image).fitted_within(size(maxWidth, maxHeight)); 364 | image.resize(new_size.width, new_size.height, -100, -100, 365 | 5); 366 | } 367 | // the actual magic which generates the output 368 | printImage(image, flags); 369 | } catch (cimg_library::CImgIOException &e) { 370 | std::cerr << "Error: '" << filename 371 | << "' has an unrecognized file format" << std::endl; 372 | ret = EXITCODE_DATA_FORMAT_ERROR; 373 | } 374 | } 375 | } else { // Thumbnail mode 376 | unsigned int index = 0; 377 | int cw = (((maxWidth / 4) - 2 * (columns - 1)) / columns); 378 | int tw = cw * 4; 379 | cimg_library::CImg image( 380 | tw * columns + 2 * 4 * (columns - 1), tw, 1, 3); 381 | size maxThumbSize(tw, tw); 382 | 383 | while (index < file_names.size()) { 384 | image.fill(0); 385 | int count = 0; 386 | std::string sb; 387 | while (index < file_names.size() && count < columns) { 388 | std::string name = file_names[index++]; 389 | try { 390 | cimg_library::CImg original = 391 | load_rgb_CImg(name.c_str(), bgColor); 392 | auto cut = name.find_last_of("/"); 393 | sb += 394 | cut == std::string::npos ? name : name.substr(cut + 1); 395 | size newSize = size(original).fitted_within(maxThumbSize); 396 | original.resize(newSize.width, newSize.height, 1, -100, 5); 397 | image.draw_image( 398 | count * (tw + 8) + (tw - newSize.width) / 2, 399 | (tw - newSize.height) / 2, 0, 0, original); 400 | count++; 401 | unsigned int sl = count * (cw + 2); 402 | sb.resize(sl - 2, ' '); 403 | sb += " "; 404 | } catch (std::exception &e) { 405 | // Probably no image; ignore. 406 | } 407 | } 408 | if (count) printImage(image, flags); 409 | std::cout << sb << std::endl << std::endl; 410 | } 411 | } 412 | return ret; 413 | } 414 | -------------------------------------------------------------------------------- /src/tiv_lib.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2023, Stefan Haustein, Aaron Liu 3 | * 4 | * This file is free software: you may copy, redistribute and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. 8 | * 9 | * This file is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | * Alternatively, you may copy, redistribute and/or modify this file under 18 | * the terms of the Apache License, version 2.0: 19 | * 20 | * Licensed under the Apache License, Version 2.0 (the "License"); 21 | * you may not use this file except in compliance with the License. 22 | * You may obtain a copy of the License at 23 | * 24 | * https://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software 27 | * distributed under the License is distributed on an "AS IS" BASIS, 28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | * See the License for the specific language governing permissions and 30 | * limitations under the License. 31 | */ 32 | 33 | #include "tiv_lib.h" 34 | 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | 42 | const int END_MARKER = 0; 43 | 44 | // An interleaved map of 4x8 bit character bitmaps (each hex digit represents a 45 | // row) to the corresponding Unicode character code point. 46 | constexpr unsigned int BITMAPS[] = { 47 | 0x00000000, 0x00a0, 0, 48 | 49 | // Block graphics 50 | // 0xffff0000, 0x2580, 0, // upper 1/2; redundant with inverse lower 1/2 51 | 52 | 0x0000000f, 0x2581, 0, // lower 1/8 53 | 0x000000ff, 0x2582, 0, // lower 1/4 54 | 0x00000fff, 0x2583, 0, 55 | 0x0000ffff, 0x2584, 0, // lower 1/2 56 | 0x000fffff, 0x2585, 0, 57 | 0x00ffffff, 0x2586, 0, // lower 3/4 58 | 0x0fffffff, 0x2587, 0, 59 | // 0xffffffff, 0x2588, // full; redundant with inverse space 60 | 61 | 0xeeeeeeee, 0x258a, 0, // left 3/4 62 | 0xcccccccc, 0x258c, 0, // left 1/2 63 | 0x88888888, 0x258e, 0, // left 1/4 64 | 65 | 0x0000cccc, 0x2596, 0, // quadrant lower left 66 | 0x00003333, 0x2597, 0, // quadrant lower right 67 | 0xcccc0000, 0x2598, 0, // quadrant upper left 68 | // 0xccccffff, 0x2599, // 3/4 redundant with inverse 1/4 69 | 0xcccc3333, 0x259a, 0, // diagonal 1/2 70 | // 0xffffcccc, 0x259b, // 3/4 redundant 71 | // 0xffff3333, 0x259c, // 3/4 redundant 72 | 0x33330000, 0x259d, 0, // quadrant upper right 73 | // 0x3333cccc, 0x259e, // 3/4 redundant 74 | // 0x3333ffff, 0x259f, // 3/4 redundant 75 | 76 | // Line drawing subset: no double lines, no complex light lines 77 | 78 | 0x000ff000, 0x2501, 0, // Heavy horizontal 79 | 0x66666666, 0x2503, 0, // Heavy vertical 80 | 81 | 0x00077666, 0x250f, 0, // Heavy down and right 82 | 0x000ee666, 0x2513, 0, // Heavy down and left 83 | 0x66677000, 0x2517, 0, // Heavy up and right 84 | 0x666ee000, 0x251b, 0, // Heavy up and left 85 | 86 | 0x66677666, 0x2523, 0, // Heavy vertical and right 87 | 0x666ee666, 0x252b, 0, // Heavy vertical and left 88 | 0x000ff666, 0x2533, 0, // Heavy down and horizontal 89 | 0x666ff000, 0x253b, 0, // Heavy up and horizontal 90 | 0x666ff666, 0x254b, 0, // Heavy cross 91 | 92 | 0x000cc000, 0x2578, 0, // Bold horizontal left 93 | 0x00066000, 0x2579, 0, // Bold horizontal up 94 | 0x00033000, 0x257a, 0, // Bold horizontal right 95 | 0x00066000, 0x257b, 0, // Bold horizontal down 96 | 97 | 0x06600660, 0x254f, 0, // Heavy double dash vertical 98 | 99 | 0x000f0000, 0x2500, 0, // Light horizontal 100 | 0x0000f000, 0x2500, 0, // 101 | 0x44444444, 0x2502, 0, // Light vertical 102 | 0x22222222, 0x2502, 0, 103 | 104 | 0x000e0000, 0x2574, 0, // light left 105 | 0x0000e000, 0x2574, 0, // light left 106 | 0x44440000, 0x2575, 0, // light up 107 | 0x22220000, 0x2575, 0, // light up 108 | 0x00030000, 0x2576, 0, // light right 109 | 0x00003000, 0x2576, 0, // light right 110 | 0x00004444, 0x2577, 0, // light down 111 | 0x00002222, 0x2577, 0, // light down 112 | 113 | // Misc technical 114 | 115 | 0x44444444, 0x23a2, 0, // [ extension 116 | 0x22222222, 0x23a5, 0, // ] extension 117 | 118 | 0x0f000000, 0x23ba, 0, // Horizontal scanline 1 119 | 0x00f00000, 0x23bb, 0, // Horizontal scanline 3 120 | 0x00000f00, 0x23bc, 0, // Horizontal scanline 7 121 | 0x000000f0, 0x23bd, 0, // Horizontal scanline 9 122 | 123 | // Geometrical shapes. Tricky because some of them are too wide. 124 | 125 | // 0x00ffff00, 0x25fe, 0, // Black medium small square 126 | 0x00066000, 0x25aa, 0, // Black small square 127 | 128 | // 0x11224488, 0x2571, 0, // diagonals 129 | // 0x88442211, 0x2572, 0, 130 | // 0x99666699, 0x2573, 0, 131 | // 0x000137f0, 0x25e2, 0, // Triangles 132 | // 0x0008cef0, 0x25e3, 0, 133 | // 0x000fec80, 0x25e4, 0, 134 | // 0x000f7310, 0x25e5, 0, 135 | 136 | // Teletext / legacy graphics 3x2 block character codes. 137 | // Using a 3-2-3 pattern consistently, perhaps we should create automatic 138 | // variations.... 139 | 140 | 0xccc00000, 0xfb00, FLAG_TELETEXT, 141 | 0x33300000, 0xfb01, FLAG_TELETEXT, 142 | 0xfff00000, 0xfb02, FLAG_TELETEXT, 143 | 0x000cc000, 0xfb03, FLAG_TELETEXT, 144 | 0xccccc000, 0xfb04, FLAG_TELETEXT, 145 | 0x333cc000, 0xfb05, FLAG_TELETEXT, 146 | 0xfffcc000, 0xfb06, FLAG_TELETEXT, 147 | 0x00033000, 0xfb07, FLAG_TELETEXT, 148 | 0xccc33000, 0xfb08, FLAG_TELETEXT, 149 | 0x33333000, 0xfb09, FLAG_TELETEXT, 150 | 0xfff33000, 0xfb0a, FLAG_TELETEXT, 151 | 0x000ff000, 0xfb0b, FLAG_TELETEXT, 152 | 0xcccff000, 0xfb0c, FLAG_TELETEXT, 153 | 0x333ff000, 0xfb0d, FLAG_TELETEXT, 154 | 0xfffff000, 0xfb0e, FLAG_TELETEXT, 155 | 0x00000ccc, 0xfb0f, FLAG_TELETEXT, 156 | 157 | 0xccc00ccc, 0xfb10, FLAG_TELETEXT, 158 | 0x33300ccc, 0xfb11, FLAG_TELETEXT, 159 | 0xfff00ccc, 0xfb12, FLAG_TELETEXT, 160 | 0x000ccccc, 0xfb13, FLAG_TELETEXT, 161 | 0x333ccccc, 0xfb14, FLAG_TELETEXT, 162 | 0xfffccccc, 0xfb15, FLAG_TELETEXT, 163 | 0x00033ccc, 0xfb16, FLAG_TELETEXT, 164 | 0xccc33ccc, 0xfb17, FLAG_TELETEXT, 165 | 0x33333ccc, 0xfb18, FLAG_TELETEXT, 166 | 0xfff33ccc, 0xfb19, FLAG_TELETEXT, 167 | 0x000ffccc, 0xfb1a, FLAG_TELETEXT, 168 | 0xcccffccc, 0xfb1b, FLAG_TELETEXT, 169 | 0x333ffccc, 0xfb1c, FLAG_TELETEXT, 170 | 0xfffffccc, 0xfb1d, FLAG_TELETEXT, 171 | 0x00000333, 0xfb1e, FLAG_TELETEXT, 172 | 0xccc00333, 0xfb1f, FLAG_TELETEXT, 173 | 174 | 0x33300333, 0x1b20, FLAG_TELETEXT, 175 | 0xfff00333, 0x1b21, FLAG_TELETEXT, 176 | 0x000cc333, 0x1b22, FLAG_TELETEXT, 177 | 0xccccc333, 0x1b23, FLAG_TELETEXT, 178 | 0x333cc333, 0x1b24, FLAG_TELETEXT, 179 | 0xfffcc333, 0x1b25, FLAG_TELETEXT, 180 | 0x00033333, 0x1b26, FLAG_TELETEXT, 181 | 0xccc33333, 0x1b27, FLAG_TELETEXT, 182 | 0xfff33333, 0x1b28, FLAG_TELETEXT, 183 | 0x000ff333, 0x1b29, FLAG_TELETEXT, 184 | 0xcccff333, 0x1b2a, FLAG_TELETEXT, 185 | 0x333ff333, 0x1b2b, FLAG_TELETEXT, 186 | 0xfffff333, 0x1b2c, FLAG_TELETEXT, 187 | 0x00000fff, 0x1b2d, FLAG_TELETEXT, 188 | 0xccc00fff, 0x1b2e, FLAG_TELETEXT, 189 | 0x33300fff, 0x1b2f, FLAG_TELETEXT, 190 | 191 | 0xfff00fff, 0x1b30, FLAG_TELETEXT, 192 | 0x000ccfff, 0x1b31, FLAG_TELETEXT, 193 | 0xcccccfff, 0x1b32, FLAG_TELETEXT, 194 | 0x333ccfff, 0x1b33, FLAG_TELETEXT, 195 | 0xfffccfff, 0x1b34, FLAG_TELETEXT, 196 | 0x00033fff, 0x1b35, FLAG_TELETEXT, 197 | 0xccc33fff, 0x1b36, FLAG_TELETEXT, 198 | 0x33333fff, 0x1b37, FLAG_TELETEXT, 199 | 0xfff33fff, 0x1b38, FLAG_TELETEXT, 200 | 0x000fffff, 0x1b39, FLAG_TELETEXT, 201 | 0xcccfffff, 0x1b3a, FLAG_TELETEXT, 202 | 0x333fffff, 0x1b3b, FLAG_TELETEXT, 203 | 204 | 0, END_MARKER, 0 // End marker 205 | }; 206 | 207 | // The channel indices are 0, 1, 2 for R, G, B 208 | unsigned char get_channel(unsigned long rgb, int index) { 209 | return (unsigned char) ((rgb >> ((2 - index) * 8)) & 255); 210 | } 211 | 212 | CharData createCharData(GetPixelFunction get_pixel, int x0, int y0, 213 | int codepoint, int pattern) { 214 | CharData result; 215 | result.codePoint = codepoint; 216 | int fg_count = 0; 217 | int bg_count = 0; 218 | unsigned int mask = 0x80000000; 219 | 220 | for (int y = 0; y < 8; y++) { 221 | for (int x = 0; x < 4; x++) { 222 | int *avg; 223 | if (pattern & mask) { 224 | avg = result.fgColor.data(); 225 | fg_count++; 226 | } else { 227 | avg = result.bgColor.data(); 228 | bg_count++; 229 | } 230 | long rgb = get_pixel(x0 + x, y0 + y); 231 | for (int i = 0; i < 3; i++) { 232 | avg[i] += get_channel(rgb, i); 233 | } 234 | mask = mask >> 1; 235 | } 236 | } 237 | 238 | // Calculate the average color value for each bucket 239 | for (int i = 0; i < 3; i++) { 240 | if (bg_count != 0) { 241 | result.bgColor[i] /= bg_count; 242 | } 243 | if (fg_count != 0) { 244 | result.fgColor[i] /= fg_count; 245 | } 246 | } 247 | return result; 248 | } 249 | 250 | CharData findCharData(GetPixelFunction get_pixel, int x0, int y0, 251 | const int &flags) { 252 | int min[3] = {255, 255, 255}; 253 | int max[3] = {0}; 254 | std::map count_per_color; 255 | 256 | // Determine the minimum and maximum value for each color channel 257 | for (int y = 0; y < 8; y++) { 258 | for (int x = 0; x < 4; x++) { 259 | long color = 0; 260 | long rgb = get_pixel(x0 + x, y0 + y); 261 | for (int i = 0; i < 3; i++) { 262 | int d = get_channel(rgb, i); 263 | min[i] = std::min(min[i], d); 264 | max[i] = std::max(max[i], d); 265 | color = (color << 8) | d; 266 | } 267 | count_per_color[color]++; 268 | } 269 | } 270 | 271 | std::multimap color_per_count; 272 | for (auto i = count_per_color.begin(); i != count_per_color.end(); ++i) { 273 | color_per_count.insert(std::pair(i->second, i->first)); 274 | } 275 | 276 | auto iter = color_per_count.rbegin(); 277 | int count2 = iter->first; 278 | long max_count_color_1 = iter->second; 279 | long max_count_color_2 = max_count_color_1; 280 | if ((++iter) != color_per_count.rend()) { 281 | count2 += iter->first; 282 | max_count_color_2 = iter->second; 283 | } 284 | 285 | unsigned int bits = 0; 286 | bool direct = count2 > (8 * 4) / 2; 287 | 288 | if (direct) { 289 | for (int y = 0; y < 8; y++) { 290 | for (int x = 0; x < 4; x++) { 291 | bits = bits << 1; 292 | int d1 = 0; 293 | int d2 = 0; 294 | unsigned long rgb = get_pixel(x0 + x, y0 + y); 295 | for (int i = 0; i < 3; i++) { 296 | int shift = 16 - 8 * i; 297 | int c1 = (max_count_color_1 >> shift) & 255; 298 | int c2 = (max_count_color_2 >> shift) & 255; 299 | int c = get_channel(rgb, i); 300 | d1 += (c1 - c) * (c1 - c); 301 | d2 += (c2 - c) * (c2 - c); 302 | } 303 | if (d1 > d2) { 304 | bits |= 1; 305 | } 306 | } 307 | } 308 | } else { 309 | // Determine the color channel with the greatest range. 310 | int splitIndex = 0; 311 | int bestSplit = 0; 312 | for (int i = 0; i < 3; i++) { 313 | if (max[i] - min[i] > bestSplit) { 314 | bestSplit = max[i] - min[i]; 315 | splitIndex = i; 316 | } 317 | } 318 | 319 | // We just split at the middle of the interval instead of computing the 320 | // median. 321 | int splitValue = min[splitIndex] + bestSplit / 2; 322 | 323 | // Compute a bitmap using the given split and sum the color values for 324 | // both buckets. 325 | for (int y = 0; y < 8; y++) { 326 | for (int x = 0; x < 4; x++) { 327 | bits = bits << 1; 328 | if (get_channel(get_pixel(x0 + x, y0 + y), 329 | splitIndex) > splitValue) { 330 | bits |= 1; 331 | } 332 | } 333 | } 334 | } 335 | 336 | // Find the best bitmap match by counting the bits that don't match, 337 | // including the inverted bitmaps. 338 | int best_diff = 8; 339 | unsigned int best_pattern = 0x0000ffff; 340 | int codepoint = 0x2584; 341 | bool inverted = false; 342 | for (int i = 0; BITMAPS[i + 1] != END_MARKER; i += 3) { 343 | if ((BITMAPS[i + 2] & flags) != BITMAPS[i + 2]) { 344 | continue; 345 | } 346 | unsigned int pattern = BITMAPS[i]; 347 | for (int j = 0; j < 2; j++) { 348 | int diff = (std::bitset<32>(pattern ^ bits)).count(); 349 | if (diff < best_diff) { 350 | best_pattern = BITMAPS[i]; // pattern might be inverted. 351 | codepoint = BITMAPS[i + 1]; 352 | best_diff = diff; 353 | inverted = best_pattern != pattern; 354 | } 355 | pattern = ~pattern; 356 | } 357 | } 358 | 359 | if (direct) { 360 | CharData result; 361 | if (inverted) { 362 | long tmp = max_count_color_1; 363 | max_count_color_1 = max_count_color_2; 364 | max_count_color_2 = tmp; 365 | } 366 | for (int i = 0; i < 3; i++) { 367 | int shift = 16 - 8 * i; 368 | result.fgColor[i] = (max_count_color_2 >> shift) & 255; 369 | result.bgColor[i] = (max_count_color_1 >> shift) & 255; 370 | result.codePoint = codepoint; 371 | } 372 | return result; 373 | } 374 | return createCharData(get_pixel, x0, y0, codepoint, best_pattern); 375 | } 376 | 377 | int clamp_byte(int value) { 378 | return value < 0 ? 0 : (value > 255 ? 255 : value); 379 | } 380 | 381 | double sqr(double n) { return n * n; } 382 | 383 | int best_index(int value, const int STEPS[], int count) { 384 | int best_diff = std::abs(STEPS[0] - value); 385 | int result = 0; 386 | for (int i = 1; i < count; i++) { 387 | int diff = std::abs(STEPS[i] - value); 388 | if (diff < best_diff) { 389 | result = i; 390 | best_diff = diff; 391 | } 392 | } 393 | return result; 394 | } 395 | -------------------------------------------------------------------------------- /src/tiv_lib.h: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (c) 2017-2023, Stefan Haustein, Aaron Liu 4 | * 5 | * This file is free software: you may copy, redistribute and/or modify it 6 | * under the terms of the GNU General Public License as published by the 7 | * Free Software Foundation, either version 3 of the License, or (at your 8 | * option) any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but 11 | * WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * Alternatively, you may copy, redistribute and/or modify this file under 19 | * the terms of the Apache License, version 2.0: 20 | * 21 | * Licensed under the Apache License, Version 2.0 (the "License"); 22 | * you may not use this file except in compliance with the License. 23 | * You may obtain a copy of the License at 24 | * 25 | * https://www.apache.org/licenses/LICENSE-2.0 26 | * 27 | * Unless required by applicable law or agreed to in writing, software 28 | * distributed under the License is distributed on an "AS IS" BASIS, 29 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 30 | * See the License for the specific language governing permissions and 31 | * limitations under the License. 32 | */ 33 | 34 | #ifndef TIV_LIB_H 35 | #define TIV_LIB_H 36 | 37 | 38 | #include 39 | #include 40 | 41 | // Implementation of flag representation for flags in the main() method 42 | constexpr int FLAG_FG = 1; 43 | constexpr int FLAG_BG = 2; 44 | constexpr int FLAG_MODE_256 = 4; // Limit colors to 256-color mode 45 | constexpr int FLAG_24BIT = 8; // 24-bit color mode 46 | constexpr int FLAG_NOOPT = 16; // Only use the same half-block character 47 | constexpr int FLAG_TELETEXT = 32; // Use teletext characters 48 | 49 | 50 | // Color saturation value steps from 0 to 255 51 | constexpr int COLOR_STEP_COUNT = 6; 52 | constexpr int COLOR_STEPS[COLOR_STEP_COUNT] = {0, 0x5f, 0x87, 0xaf, 0xd7, 0xff}; 53 | 54 | // Grayscale saturation value steps from 0 to 255 55 | constexpr int GRAYSCALE_STEP_COUNT = 24; 56 | constexpr int GRAYSCALE_STEPS[GRAYSCALE_STEP_COUNT] = { 57 | 0x08, 0x12, 0x1c, 0x26, 0x30, 0x3a, 0x44, 0x4e, 0x58, 0x62, 0x6c, 0x76, 58 | 0x80, 0x8a, 0x94, 0x9e, 0xa8, 0xb2, 0xbc, 0xc6, 0xd0, 0xda, 0xe4, 0xee}; 59 | 60 | 61 | typedef std::function GetPixelFunction; 62 | 63 | unsigned char get_channel(unsigned long rgb, int index); 64 | 65 | int clamp_byte(int value); 66 | 67 | int best_index(int value, const int STEPS[], int count); 68 | 69 | double sqr(double n); 70 | 71 | /** 72 | * @brief Struct to represent a character to be drawn. 73 | * @param fgColor RGB 74 | * @param bgColor RGB 75 | * @param codePoint The code point of the character to be drawn. 76 | */ 77 | struct CharData { 78 | std::array fgColor = std::array{0, 0, 0}; 79 | std::array bgColor = std::array{0, 0, 0}; 80 | int codePoint; 81 | }; 82 | 83 | // Return a CharData struct with the given code point and corresponding averag 84 | // fg and bg colors. 85 | CharData createCharData(GetPixelFunction get_pixel, int x0, int y0, 86 | int codepoint, int pattern); 87 | 88 | /** 89 | * @brief Find the best character and colors 90 | * for a 4x8 part of the image at the given position 91 | * 92 | * @param image 93 | * @param x0 94 | * @param y0 95 | * @param flags 96 | * @return CharData 97 | */ 98 | CharData findCharData(GetPixelFunction get_pixel, int x0, int y0, 99 | const int &flags); 100 | 101 | #endif 102 | -------------------------------------------------------------------------------- /terminalimageviewer.rb: -------------------------------------------------------------------------------- 1 | class Terminalimageviewer < Formula 2 | desc "Display images in a terminal using block graphic characters" 3 | homepage "https://github.com/stefanhaustein/TerminalImageViewer" 4 | url "https://github.com/stefanhaustein/TerminalImageViewer/archive/refs/tags/v1.1.1.tar.gz" 5 | sha256 "9a5f5c8688ef8db0e88dfcea6a1ae30da32268a7ab7972ff0de71955a75af0db" 6 | license "Apache-2.0" 7 | head "https://github.com/stefanhaustein/TerminalImageViewer.git", branch: "master" 8 | 9 | depends_on "imagemagick" 10 | 11 | def install 12 | cd "src/main/cpp" do 13 | system "/usr/local/bin/g++-#{Formula["gcc"].version_suffix}", "-std=c++17", "-Wall", "-fpermissive", 14 | "-fexceptions", "-O2", "-c", "-L/usr/local/opt/gcc/lib/gcc/#{Formula["gcc"].version_suffix}/", "tiv.cpp", 15 | "-o", "tiv.o" 16 | system "/usr/local/bin/g++-#{Formula["gcc"].version_suffix}", "tiv.o", "-o", "tiv", 17 | "-L/usr/local/opt/gcc/lib/gcc/#{Formula["gcc"].version_suffix}/", "-pthread", "-s" 18 | bin.install "tiv" 19 | end 20 | end 21 | 22 | test do 23 | assert_equal "\e[48;2;0;0;255m\e[38;2;0;0;255m \e[0m", 24 | shell_output("#{bin}/tiv #{test_fixtures("test.png")}").strip 25 | end 26 | end 27 | --------------------------------------------------------------------------------