1 /*
2 icat -- Outputs an image on a 256-color or 24 bit color enabled terminal with UTF-8 locale
3 Andreas Textor <textor.andreas@googlemail.com>
4 
5 Compile: gcc -Wall -pedantic -std=c99 -D_BSD_SOURCE -o icat icat.c -lImlib2
6 
7 Copyright (c) 2012 Andreas Textor. All rights reserved.
8 
9 Redistribution and use in source and binary forms, with or without
10 modification, are permitted provided that the following conditions are met:
11 
12    1. Redistributions of source code must retain the above copyright notice, this
13       list of conditions and the following disclaimer.
14 
15    2. Redistributions in binary form must reproduce the above copyright notice,
16       this list of conditions and the following disclaimer in the documentation
17       and/or other materials provided with the distribution.
18 
19 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
23 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30 
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <stdint.h>
34 #include <stdbool.h>
35 #include <string.h>
36 #include <getopt.h>
37 #include <unistd.h>
38 #include <sys/ioctl.h>
39 #include <Imlib2.h>
40 
41 #define VERSION "0.5"
42 
43 enum {MODE_NOTHING, MODE_INDEXED, MODE_24_BIT, MODE_BOTH};
44 
45 static uint32_t colors[] = {
46 	// Colors 0 to 15: original ANSI colors
47 	0x000000, 0xcd0000, 0x00cd00, 0xcdcd00, 0x0000ee, 0xcd00cd, 0x00cdcd, 0xe5e5e5,
48 	0x7f7f7f, 0xff0000, 0x00ff00, 0xffff00, 0x5c5cff, 0xff00ff, 0x00ffff, 0xffffff,
49 	// Color cube colors
50 	0x000000, 0x00005f, 0x000087, 0x0000af, 0x0000d7, 0x0000ff, 0x005f00, 0x005f5f,
51 	0x005f87, 0x005faf, 0x005fd7, 0x005fff, 0x008700, 0x00875f, 0x008787, 0x0087af,
52 	0x0087d7, 0x0087ff, 0x00af00, 0x00af5f, 0x00af87, 0x00afaf, 0x00afd7, 0x00afff,
53 	0x00d700, 0x00d75f, 0x00d787, 0x00d7af, 0x00d7d7, 0x00d7ff, 0x00ff00, 0x00ff5f,
54 	0x00ff87, 0x00ffaf, 0x00ffd7, 0x00ffff, 0x5f0000, 0x5f005f, 0x5f0087, 0x5f00af,
55 	0x5f00d7, 0x5f00ff, 0x5f5f00, 0x5f5f5f, 0x5f5f87, 0x5f5faf, 0x5f5fd7, 0x5f5fff,
56 	0x5f8700, 0x5f875f, 0x5f8787, 0x5f87af, 0x5f87d7, 0x5f87ff, 0x5faf00, 0x5faf5f,
57 	0x5faf87, 0x5fafaf, 0x5fafd7, 0x5fafff, 0x5fd700, 0x5fd75f, 0x5fd787, 0x5fd7af,
58 	0x5fd7d7, 0x5fd7ff, 0x5fff00, 0x5fff5f, 0x5fff87, 0x5fffaf, 0x5fffd7, 0x5fffff,
59 	0x870000, 0x87005f, 0x870087, 0x8700af, 0x8700d7, 0x8700ff, 0x875f00, 0x875f5f,
60 	0x875f87, 0x875faf, 0x875fd7, 0x875fff, 0x878700, 0x87875f, 0x878787, 0x8787af,
61 	0x8787d7, 0x8787ff, 0x87af00, 0x87af5f, 0x87af87, 0x87afaf, 0x87afd7, 0x87afff,
62 	0x87d700, 0x87d75f, 0x87d787, 0x87d7af, 0x87d7d7, 0x87d7ff, 0x87ff00, 0x87ff5f,
63 	0x87ff87, 0x87ffaf, 0x87ffd7, 0x87ffff, 0xaf0000, 0xaf005f, 0xaf0087, 0xaf00af,
64 	0xaf00d7, 0xaf00ff, 0xaf5f00, 0xaf5f5f, 0xaf5f87, 0xaf5faf, 0xaf5fd7, 0xaf5fff,
65 	0xaf8700, 0xaf875f, 0xaf8787, 0xaf87af, 0xaf87d7, 0xaf87ff, 0xafaf00, 0xafaf5f,
66 	0xafaf87, 0xafafaf, 0xafafd7, 0xafafff, 0xafd700, 0xafd75f, 0xafd787, 0xafd7af,
67 	0xafd7d7, 0xafd7ff, 0xafff00, 0xafff5f, 0xafff87, 0xafffaf, 0xafffd7, 0xafffff,
68 	0xd70000, 0xd7005f, 0xd70087, 0xd700af, 0xd700d7, 0xd700ff, 0xd75f00, 0xd75f5f,
69 	0xd75f87, 0xd75faf, 0xd75fd7, 0xd75fff, 0xd78700, 0xd7875f, 0xd78787, 0xd787af,
70 	0xd787d7, 0xd787ff, 0xd7af00, 0xd7af5f, 0xd7af87, 0xd7afaf, 0xd7afd7, 0xd7afff,
71 	0xd7d700, 0xd7d75f, 0xd7d787, 0xd7d7af, 0xd7d7d7, 0xd7d7ff, 0xd7ff00, 0xd7ff5f,
72 	0xd7ff87, 0xd7ffaf, 0xd7ffd7, 0xd7ffff, 0xff0000, 0xff005f, 0xff0087, 0xff00af,
73 	0xff00d7, 0xff00ff, 0xff5f00, 0xff5f5f, 0xff5f87, 0xff5faf, 0xff5fd7, 0xff5fff,
74 	0xff8700, 0xff875f, 0xff8787, 0xff87af, 0xff87d7, 0xff87ff, 0xffaf00, 0xffaf5f,
75 	0xffaf87, 0xffafaf, 0xffafd7, 0xffafff, 0xffd700, 0xffd75f, 0xffd787, 0xffd7af,
76 	0xffd7d7, 0xffd7ff, 0xffff00, 0xffff5f, 0xffff87, 0xffffaf, 0xffffd7, 0xffffff,
77 	// >= 233: Grey ramp
78 	0x000000, 0x121212, 0x1c1c1c, 0x262626, 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e,
79 	0x585858, 0x626262, 0x6c6c6c, 0x767676, 0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e,
80 	0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada,
81 };
82 
83 // Find an xterm color value that matches an ARGB color
rgb2xterm(Imlib_Color * pixel)84 uint8_t rgb2xterm(Imlib_Color* pixel) {
85 	uint8_t c, match = 0;
86 	uint32_t r, g, b, d, distance;
87 
88 	distance = 1000000000;
89 	for(c = 0; c <= 253; c++) {
90 		r = ((0xff0000 & colors[c]) >> 16) - pixel->red;
91 		g = ((0x00ff00 & colors[c]) >> 8)  - pixel->green;
92 		b = (0x0000ff & colors[c]) - pixel->blue;
93 		d = r * r + g * g + b * b;
94 		if (d < distance) {
95 			distance = d;
96 			match = c;
97 		}
98 	}
99 
100 	return match;
101 }
102 
print_usage()103 void print_usage() {
104 	printf("icat (" VERSION ") outputs an image on a 256-color or 24-bit color enabled terminal with UTF-8 locale.\n"
105 			"Usage: icat [-h|--help] [-x value] [-y value] [-w|--width value] [-k|--keep]\n"
106 			"            [-m|--mode indexed|24bit|both] imagefile [imagefile...]\n"
107 			"	-h | --help  -- Display this message\n"
108 			"	-x value     -- Specify the column to print the image in (min. 1)\n"
109 			"	-y value     -- Specify the row to print the image in (min. 1)\n"
110 			"	                This is ignored when more than one image is printed.\n"
111 			"	-w | --width value\n"
112 			"	                Instead of resizing the image to fit the terminal width,\n"
113 			"	                use the provided value as the desired width.\n"
114 			"	-k | --keep  -- Keep image size, i.e. do not automatically resize image to fit\n"
115 			"	                the terminal width.\n"
116 			"	-m | --mode indexed|24bit|both\n"
117 			"	                Use indexed (256-color, the default), 24-bit color, or both.\n"
118 			"	imagefile    -- The image to print. If the file name is \"-\", the file is\n"
119 			"	                read from stdin.\n"
120 			"Big images are automatically resized to your terminal width, unless with the -k option.\n"
121 			"You can achieve the same effect with convert (from the ImageMagick package):\n"
122 			"	convert -resize $((COLUMNS - 2))x image.png - | icat -\n"
123 			"Or read images from another source:\n"
124 			"	curl -sL http://example.com/image.png | convert -resize $((COLUMNS - 2))x - - | icat -\n");
125 }
126 
127 // Find out and return the number of columns in the terminal
terminal_width()128 int terminal_width() {
129 	int cols = 80;
130 #ifdef TIOCGSIZE
131 	struct ttysize ts;
132 	ioctl(STDIN_FILENO, TIOCGSIZE, &ts);
133 	cols = ts.ts_cols;
134 #elif defined(TIOCGWINSZ)
135 	struct winsize ts;
136 	ioctl(STDIN_FILENO, TIOCGWINSZ, &ts);
137 	cols = ts.ws_col;
138 #endif /* TIOCGSIZE */
139 	return cols;
140 }
141 
142 // Resize image, but only if the user explicitly provided the image width,
143 // or the image wouldn't fit in the terminal.
resize_image_if_necessary(int * width,int * height,const int user_width)144 void resize_image_if_necessary(int *width, int *height, const int user_width) {
145 	int resized_width;
146 
147 	if (!user_width) {
148 		int cols = terminal_width();
149 		if(cols > *width) return;
150 
151 		resized_width = cols;
152 	} else {
153 		resized_width = user_width;
154 	}
155 
156 	float ratio = ((float)resized_width) / *width;
157 	int resized_height = *height * ratio;
158 
159 	Imlib_Image resized_image = imlib_create_cropped_scaled_image(0, 0,
160 			*width, *height, resized_width, resized_height);
161 	imlib_free_image_and_decache();
162 	imlib_context_set_image(resized_image);
163 
164 	*width = resized_width;
165 	*height = resized_height;
166 }
167 
168 // Prints two pixels inside one character, p1 below p2.
169 // Characters in terminal fonts are usually twice as high
170 // as they are wide.
print_pixels(Imlib_Color * p1,Imlib_Color * p2,int mode)171 void print_pixels(Imlib_Color* p1, Imlib_Color* p2, int mode) {
172 	// Newer xterms should support 24-bit color with the ESC[38;2;<r>;<g>;<b>m sequence.
173 	// For backward compatibility, we insert the old ESC[38;5;<color_index>m before it.
174 	static char* upper = "▀";
175 	static char* lower = "▄";
176 	if (p1->alpha == 0 && p2->alpha == 0) {
177 		// Both pixels are transparent
178 		printf("\x1b[0m ");
179 	} else if (p1->alpha == 0 && p2->alpha != 0) {
180 		// Only lower pixel is transparent
181 		fputs("\x1b[0m", stdout);
182 		if(mode & MODE_INDEXED) {
183 			uint8_t col2 = rgb2xterm(p2);
184 			printf("\x1b[38;5;%dm", col2);
185 		}
186 		if(mode & MODE_24_BIT) {
187 			printf("\x1b[38;2;%d;%d;%dm", p2->red, p2->green, p2->blue);
188 		}
189 		fputs(upper, stdout);
190 	} else if (p1->alpha != 0 && p2->alpha == 0) {
191 		// Only upper pixel is transparent
192 		fputs("\x1b[0m", stdout);
193 		if(mode & MODE_INDEXED) {
194 			uint8_t col1 = rgb2xterm(p1);
195 			printf("\x1b[38;5;%dm", col1);
196 		}
197 		if(mode & MODE_24_BIT) {
198 			printf("\x1b[38;2;%d;%d;%dm", p1->red, p1->green, p1->blue);
199 		}
200 		fputs(lower, stdout);
201 	} else {
202 		// Both pixels are opaque
203 		if(mode & MODE_INDEXED) {
204 			uint8_t col1 = rgb2xterm(p1);
205 			uint8_t col2 = rgb2xterm(p2);
206 			printf("\x1b[38;5;%dm\x1b[48;5;%dm", col1, col2);
207 		}
208 		if(mode & MODE_24_BIT) {
209 			printf("\x1b[38;2;%d;%d;%dm\x1b[48;2;%d;%d;%dm", p1->red, p1->green, p1->blue, p2->red, p2->green, p2->blue);
210 		}
211 		fputs(lower, stdout);
212 	}
213 }
214 
main(int argc,char * argv[])215 int main(int argc, char* argv[]) {
216 	char *filename;
217 	static int display_help = 0;
218 	Imlib_Image image = NULL;
219 	unsigned int x = 0;
220 	unsigned int y = 0;
221 	unsigned int user_w = 0;
222 	int c;
223 	bool keep_size = false;
224 	int mode = MODE_INDEXED;
225 
226 	for(;;) {
227 		static struct option long_options[] = {
228 			{"help", no_argument,       &display_help, 1},
229 			{"x",    required_argument, 0, 'x'},
230 			{"y",    required_argument, 0, 'y'},
231 			{"width",required_argument, 0, 'w'},
232 			{"keep", no_argument,       0, 'k'},
233 			{"mode", required_argument, 0, 'm'},
234 			{0, 0, 0, 0}
235 		};
236 
237 		c = getopt_long(argc, argv, "hx:y:w:km:", long_options, NULL);
238 
239 		if (c == -1)
240 			break;
241 
242 		switch (c) {
243 			case 'x':
244 				x = atoi(optarg);
245 				if (x < 0) {
246 					x = 0;
247 				}
248 				break;
249 
250 			case 'y':
251 				y = atoi(optarg);
252 				if (y < 0) {
253 					y = 0;
254 				}
255 				break;
256 
257 			case 'w':
258 				user_w = atoi(optarg);
259 				if(user_w < 1) {
260 					printf("Image width must be larger than 0\n");
261 					exit(1);
262 				}
263 				break;
264 
265 			case 'h':
266 				display_help = 1;
267 				break;
268 
269 			case 'k':
270 				keep_size = true;
271 				break;
272 
273 			case 'm':
274 				if(strcmp(optarg, "indexed") == 0) {
275 					mode = MODE_INDEXED;
276 				} else if(strcmp(optarg, "24bit") == 0) {
277 					mode = MODE_24_BIT;
278 				} else if(strcmp(optarg, "both") == 0) {
279 					mode = MODE_BOTH;
280 				} else {
281 					printf("Mode must be one of 'indexed', '24bit', or 'both'.\n");
282 					exit(1);
283 				}
284 				break;
285 
286 			case '?':
287 				break;
288 
289 			default:
290 				display_help = 1;
291 				break;
292 		}
293 	}
294 
295 	if (display_help == 1 || optind == argc) {
296 		print_usage();
297 		exit(0);
298 	}
299 
300 	// Ignore y value when more than one picture is printed
301 	if (argc - optind > 1) {
302 		y = 0;
303 	}
304 
305 	for (int i = optind; i < argc; i++) {
306 		// Read from stdin and write temp file. Although a temp file
307 		// is ugly, imlib can not seek in a pipe and therefore not
308 		// read an image from it.
309 		if (strcmp(argv[i], "-") == 0) {
310 			int tempfile;
311 			char tempfile_name[] = "/tmp/icatXXXXXX";
312 			if ((tempfile = mkstemp(tempfile_name)) < 0) {
313 				perror("mkstemp");
314 				exit(EXIT_FAILURE);
315 			}
316 			filename = tempfile_name;
317 			char buf;
318 			while (read(0, &buf, 1) > 0) {
319 				write(tempfile, &buf, 1);
320 			}
321 		} else {
322 			filename = argv[i];
323 		}
324 
325 		// Load image
326 		image = imlib_load_image_immediately(filename);
327 		if (!image) {
328 			fprintf(stderr, "Could not load image: %s\n", filename);
329 			exit(EXIT_FAILURE);
330 		}
331 
332 		imlib_context_set_image(image);
333 		int width = imlib_image_get_width();
334 		int height = imlib_image_get_height();
335 
336 		// Unless told to retain image size, check if resize if needed
337 		if (!keep_size) {
338 			resize_image_if_necessary(&width, &height, user_w);
339 		}
340 
341 		// If an y-value is given, position the cursor in that line (and
342 		// given or default column)
343 		if (y > 0) {
344 			printf("\x1b[%d;%dH", y, x);
345 		}
346 
347 		Imlib_Color pixel1;
348 		Imlib_Color pixel2;
349 		for (int h = 0; h < height; h += 2) {
350 			// If an x-offset is given, position the cursor in that column
351 			if (x > 0) {
352 				printf("\x1b[%dG", x);
353 			}
354 
355 			// Draw a horizontal line. Each console line consists of two
356 			// pixel lines in order to keep the pixels square (most console
357 			// fonts have two times the height for the width of each character)
358 			for (int w = 0; w < width; w++) {
359 				imlib_image_query_pixel(w, h + 1, &pixel1);
360 				imlib_image_query_pixel(w, h, &pixel2);
361 				print_pixels(&pixel1, &pixel2, mode);
362 			}
363 			printf("\x1b[0m\n");
364 		}
365 
366 		imlib_free_image_and_decache();
367 	}
368 	return 0;
369 }
370