ansilove

ANSI and ASCII art to PNG converter in C
Log | Files | Refs | README | LICENSE

ansilove.c (11753B)


      1 /*
      2  * ansilove.c
      3  * Ansilove 4.1.4
      4  * https://www.ansilove.org
      5  *
      6  * Copyright (c) 2011-2020 Stefan Vogt, Brian Cassidy, and Frederic Cambus
      7  * All rights reserved.
      8  *
      9  * Ansilove is licensed under the BSD 2-Clause License.
     10  * See LICENSE file for details.
     11  */
     12 
     13 #include <sys/time.h>
     14 #include <ansilove.h>
     15 #include <err.h>
     16 #include <getopt.h>
     17 #include <stdbool.h>
     18 #include <stdint.h>
     19 #include <stdio.h>
     20 #include <stdlib.h>
     21 #include <string.h>
     22 #include <time.h>
     23 
     24 #ifdef HAVE_SECCOMP
     25 #include <sys/prctl.h>
     26 #include <linux/seccomp.h>
     27 #include "seccomp.h"
     28 #endif
     29 
     30 #include "compat.h"
     31 #include "config.h"
     32 #include "fonts.h"
     33 #include "sauce.h"
     34 #include "strtolower.h"
     35 #include "types.h"
     36 
     37 /* prototypes */
     38 static void synopsis(void);
     39 static void version(void);
     40 
     41 static void
     42 synopsis(void)
     43 {
     44 	fprintf(stdout, "SYNOPSIS\n"
     45 	    "     ansilove [-dhiqrsSv] [-b bits] [-c columns] [-f font]"
     46 	    " [-m mode] [-o file]\n"
     47 	    "              [-R factor] [-t type] file\n");
     48 }
     49 
     50 static void
     51 version(void)
     52 {
     53 	fprintf(stdout, "AnsiLove/C %s - ANSI / ASCII art to PNG converter\n"
     54 	    "Copyright (c) 2011-2020 Stefan Vogt, Brian Cassidy, and "
     55 	    "Frederic Cambus.\n", VERSION);
     56 }
     57 
     58 int
     59 main(int argc, char *argv[])
     60 {
     61 	FILE *messages = NULL;
     62 
     63 	/* SAUCE record related bool types */
     64 	bool justDisplaySAUCE = false;
     65 	bool fileHasSAUCE = false;
     66 	bool useSAUCEInfo = false;
     67 
     68 	int getoptFlag;
     69 
     70 	char *input = NULL, *output = NULL;
     71 	char *fileName = NULL;
     72 	char *font = NULL;
     73 	char *type = NULL;
     74 	int filetype = 0;
     75 
     76 	struct timespec begin, end, elapsed;
     77 
     78 	static struct ansilove_ctx ctx;
     79 	static struct ansilove_options options;
     80 
     81 	const char *errstr;
     82 
     83 	if (pledge("stdio cpath rpath wpath", NULL) == -1)
     84 		err(EXIT_FAILURE, "pledge");
     85 
     86 #ifdef HAVE_SECCOMP
     87 	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
     88 		perror("Can't initialize seccomp");
     89 		return EXIT_FAILURE;
     90 	}
     91 
     92 	if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &ansilove)) {
     93 		perror("Can't load seccomp filter");
     94 		return EXIT_FAILURE;
     95 	}
     96 #endif
     97 
     98 	if (ansilove_init(&ctx, &options) == -1)
     99 		errx(EXIT_FAILURE, "%s", ansilove_error(&ctx));
    100 
    101 	while ((getoptFlag = getopt(argc, argv, "b:c:df:him:o:qrR:sSt:v")) != -1) {
    102 		switch (getoptFlag) {
    103 		case 'b':
    104 			options.bits = strtonum(optarg, 8, 9, &errstr);
    105 
    106 			if (errstr)
    107 				errx(EXIT_FAILURE, "Invalid value for bits (must be 8 or 9).");
    108 
    109 			break;
    110 		case 'c':
    111 			options.columns = strtonum(optarg, 1, 4096, &errstr);
    112 
    113 			if (errstr)
    114 				errx(EXIT_FAILURE, "\nInvalid value for columns (must range from 1 to 4096).");
    115 
    116 			break;
    117 		case 'd':
    118 			options.dos = true;
    119 			break;
    120 		case 'f':
    121 			font = optarg;
    122 			break;
    123 		case 'h':
    124 			synopsis();
    125 			return EXIT_SUCCESS;
    126 		case 'i':
    127 			options.icecolors = true;
    128 			break;
    129 		case 'm':
    130 			if (!strcmp(optarg, "ced")) {
    131 				options.mode = ANSILOVE_MODE_CED;
    132 			} else if (!strcmp(optarg, "transparent")) {
    133 				options.mode = ANSILOVE_MODE_TRANSPARENT;
    134 			} else if (!strcmp(optarg, "workbench")) {
    135 				options.mode = ANSILOVE_MODE_WORKBENCH;
    136 			}
    137 			break;
    138 		case 'o':
    139 			output = optarg;
    140 			break;
    141 		case 'q':
    142 			messages = fopen("/dev/null", "w");
    143 			break;
    144 		case 'r':
    145 			options.scale_factor = 2;
    146 			break;
    147 		case 'R':
    148 			options.scale_factor = strtonum(optarg, 2, 8, &errstr);
    149 
    150 			if (errstr)
    151 				errx(EXIT_FAILURE, "Invalid value for retina scale factor (must range from 2 to 8).");
    152 
    153 			break;
    154 		case 's':
    155 			justDisplaySAUCE = true;
    156 			break;
    157 		case 'S':
    158 			useSAUCEInfo = true;
    159 			break;
    160 		case 't':
    161 			type = strtolower(optarg);
    162 			break;
    163 		case 'v':
    164 			version();
    165 			return EXIT_SUCCESS;
    166 		}
    167 	}
    168 
    169 	if (optind < argc) {
    170 		input = argv[optind];
    171 	} else {
    172 		synopsis();
    173 		return EXIT_SUCCESS;
    174 	}
    175 
    176 	argc -= optind;
    177 	argv += optind;
    178 
    179 	/* if -q flag was not set, default to stdout */
    180 	if (!messages)
    181 		messages = stdout;
    182 
    183         /* Starting timer */
    184         clock_gettime(CLOCK_MONOTONIC, &begin);
    185 
    186 	/* let's check the file for a valid SAUCE record */
    187 	struct sauce *record = sauceReadFileName(input);
    188 
    189 	/* record == NULL also means there is no file, we can stop here */
    190 	if (record == NULL) {
    191 		errx(EXIT_FAILURE, "File %s not found.", input);
    192 	} else {
    193 		/* if we find a SAUCE record, update bool flag */
    194 		if (!strcmp(record->ID, SAUCE_ID)) {
    195 			fileHasSAUCE = true;
    196 		}
    197 	}
    198 
    199 	if (!justDisplaySAUCE) {
    200 		/* gather rendering hints from SAUCE */
    201 		if (useSAUCEInfo && fileHasSAUCE) {
    202 			bool usedSAUCE = false;
    203 			if (record->dataType == 1) {
    204 				if (record->fileType == 0 || record->fileType == 1 || record->fileType == 2) {
    205 					options.columns = record->tinfo1;
    206 					if (record->flags & 1)
    207 						options.icecolors = true;
    208 					if ((record->flags & 6) == 4)
    209 						options.bits = 9;
    210 					if ((record->flags & 24) == 8)
    211 						options.dos = true;
    212 					usedSAUCE = true;
    213 				}
    214 				if (record->fileType == 8) {
    215 					options.columns = record->tinfo1;
    216 					usedSAUCE = true;
    217 				}
    218 			}
    219 			if (record->dataType == 5) {
    220 				options.columns = record->tinfo1;
    221 				usedSAUCE = true;
    222 			}
    223 			/* XBIN (dataType == 6) could also use tinfo1 for width, but we trust the XBIN header more */
    224 			/* font info */
    225 			if ((record->dataType == 1 && (record->fileType == 0 || record->fileType == 1 || record->fileType == 2)) || record->dataType == 5 ) {
    226 				if (strcmp(record->tinfos, "IBM VGA") == 0) {
    227 					font = "80x25";
    228 				}
    229 				if (strcmp(record->tinfos, "IBM VGA50") == 0) {
    230 					font = "80x50";
    231 				}
    232 				if (strcmp(record->tinfos, "IBM VGA 437") == 0) {
    233 					font = "80x25";
    234 				}
    235 				if (strcmp(record->tinfos, "IBM VGA50 437") == 0) {
    236 					font = "80x50";
    237 				}
    238 				if (strcmp(record->tinfos, "IBM VGA 775") == 0) {
    239 					font = "baltic";
    240 				}
    241 				if (strcmp(record->tinfos, "IBM VGA50 855") == 0) {
    242 					font = "cyrillic";
    243 				}
    244 				if (strcmp(record->tinfos, "IBM VGA 863") == 0) {
    245 					font = "french-canadian";
    246 				}
    247 				if (strcmp(record->tinfos, "IBM VGA 737") == 0) {
    248 					font = "greek";
    249 				}
    250 				if (strcmp(record->tinfos, "IBM VGA 869") == 0) {
    251 					font = "greek-869";
    252 				}
    253 				if (strcmp(record->tinfos, "IBM VGA 862") == 0) {
    254 					font = "hebrew";
    255 				}
    256 				if (strcmp(record->tinfos, "IBM VGA 861") == 0) {
    257 					font = "icelandic";
    258 				}
    259 				if (strcmp(record->tinfos, "IBM VGA 850") == 0) {
    260 					font = "latin1";
    261 				}
    262 				if (strcmp(record->tinfos, "IBM VGA 852") == 0) {
    263 					font = "latin2";
    264 				}
    265 				if (strcmp(record->tinfos, "IBM VGA 865") == 0) {
    266 					font = "nordic";
    267 				}
    268 				if (strcmp(record->tinfos, "IBM VGA 860") == 0) {
    269 					font = "portuguese";
    270 				}
    271 				if (strcmp(record->tinfos, "IBM VGA 866") == 0) {
    272 					font = "russian";
    273 				}
    274 				if (strcmp(record->tinfos, "IBM VGA 857") == 0) {
    275 					font = "turkish";
    276 				}
    277 				if (strcmp(record->tinfos, "Amiga MicroKnight") == 0) {
    278 					font = "microknight";
    279 				}
    280 				if (strcmp(record->tinfos, "Amiga MicroKnight+") == 0) {
    281 					font = "microknight+";
    282 				}
    283 				if (strcmp(record->tinfos, "Amiga mOsOul") == 0) {
    284 					font = "mosoul";
    285 				}
    286 				if (strcmp(record->tinfos, "Amiga P0T-NOoDLE") == 0) {
    287 					font = "pot-noodle";
    288 				}
    289 				if (strcmp(record->tinfos, "Amiga Topaz 1") == 0) {
    290 					font = " topaz500";
    291 				}
    292 				if (strcmp(record->tinfos, "Amiga Topaz 1+") == 0) {
    293 					font = "topaz500+";
    294 				}
    295 				if (strcmp(record->tinfos, "Amiga Topaz 2") == 0) {
    296 					font = "topaz";
    297 				}
    298 				if (strcmp(record->tinfos, "Amiga Topaz 2+") == 0) {
    299 					font = "topaz+";
    300 				}
    301 			}
    302 			if (usedSAUCE) {
    303 				fprintf(messages, "SAUCE info used for rendering hints\n\n");
    304 			}
    305 		}
    306 
    307 		if (font) {
    308 			for (size_t loop = 0; loop < FONTS; loop++) {
    309 				if (!strcmp(fonts[loop], font)) {
    310 					options.font = fontsId[loop];
    311 					break;
    312 				}
    313 			}
    314 		}
    315 
    316 		/* create output file name if output is not specified */
    317 		if (!output) {
    318 			/* appending ".png" extension to output file name */
    319 			if (asprintf(&fileName, "%s%s", input, ".png") == -1)
    320 				errx(EXIT_FAILURE, "Memory allocation error.");
    321 		} else {
    322 			fileName = strdup(output);
    323 		}
    324 
    325 		/* display name of input and output files */
    326 		fprintf(messages, "Input File: %s\n", input);
    327 		fprintf(messages, "Output File: %s\n", fileName);
    328 
    329 		/* get file extension */
    330 		char *fext = strrchr(input, '.');
    331 		fext = fext ? strtolower(strdup(++fext)) : strdup("");
    332 		if (!fext)
    333 			errx(EXIT_FAILURE, "Memory allocation error.");
    334 
    335 		/* check if current file has a .diz extension */
    336 		if (!strcmp(fext, "diz"))
    337 			options.diz = true;
    338 
    339 		if (ansilove_loadfile(&ctx, input) == -1)
    340 			errx(EXIT_FAILURE, "%s", ansilove_error(&ctx));
    341 
    342 		/* adjust the file size if file contains a SAUCE record */
    343 		if (fileHasSAUCE)
    344 			ctx.length -= 129 - (record->comments > 0 ? 5 + 64 * record->comments : 0);
    345 
    346 		int (*loader)(struct ansilove_ctx *, struct ansilove_options *) = NULL;
    347 
    348 		/* if type was specified, attempt to find a loader */
    349 		if (type) {
    350 			for (size_t loop = 0; loop < TYPES; loop++) {
    351 				if (!strcmp(types[loop], type)) {
    352 					loader = loaders[loop];
    353 					filetype = filetypes[loop];
    354 					break;
    355 				}
    356 			}
    357 
    358 			if (!loader)
    359 				errx(EXIT_FAILURE, "Unknown file type.");
    360 		}
    361 
    362 		/* use file extension to find a suitable loader */
    363 		if (!loader) {
    364 			for (size_t loop = 0; loop < TYPES; loop++) {
    365 				if (!strcmp(types[loop], fext)) {
    366 					loader = loaders[loop];
    367 					filetype = filetypes[loop];
    368 					break;
    369 				}
    370 			}
    371 		}
    372 
    373 		/* default to ANSI if file extension is unknown */
    374 		if (!loader) {
    375 			loader = ansilove_ansi;
    376 			filetype = ANSILOVE_FILETYPE_ANS;
    377 		}
    378 
    379 		if (loader(&ctx, &options) == -1)
    380 			errx(EXIT_FAILURE, "%s", ansilove_error(&ctx));
    381 
    382 		/* create the output file */
    383 		if (ansilove_savefile(&ctx, fileName) == -1)
    384 			errx(EXIT_FAILURE, "%s", ansilove_error(&ctx));
    385 
    386 		/* gather information and report to the command line */
    387 		switch(filetype) {
    388 		case ANSILOVE_FILETYPE_ANS:
    389 		case ANSILOVE_FILETYPE_BIN:
    390 			if (options.icecolors)
    391 				fprintf(messages, "iCE Colors: enabled\n");
    392 
    393 			/* FALLTHROUGH */
    394 		case ANSILOVE_FILETYPE_PCB:
    395 		case ANSILOVE_FILETYPE_TND:
    396 			fprintf(messages, "Font: %s\n", font ? font : "80x25");
    397 			fprintf(messages, "Bits: %d\n", options.bits);
    398 			fprintf(messages, "Columns: %d\n", options.columns);
    399 		}
    400 
    401 		if (options.scale_factor)
    402 			fprintf(messages, "Scale factor: %d\n", options.scale_factor);
    403 
    404 		free(fileName);
    405 		free(fext);
    406 	}
    407 
    408 	/* either display SAUCE or tell us if there is no record */
    409 	if (!fileHasSAUCE) {
    410 		fprintf(messages, "\nFile %s does not have a SAUCE record.\n", input);
    411 	} else {
    412 		fprintf(messages, "\nId: %s v%s\n", record->ID, record->version);
    413 		fprintf(messages, "Title: %s\n", record->title);
    414 		fprintf(messages, "Author: %s\n", record->author);
    415 		fprintf(messages, "Group: %s\n", record->group);
    416 		fprintf(messages, "Date: %s\n", record->date);
    417 		fprintf(messages, "Datatype: %d\n", record->dataType);
    418 		fprintf(messages, "Filetype: %d\n", record->fileType);
    419 
    420 		if (record->flags)
    421 			fprintf(messages, "Flags: 0b%d%d%d%d%d%d%d%d\n", record->flags >> 7, record->flags >> 6 & 1, record->flags >> 5 & 1, record->flags >> 4 & 1, record->flags >> 3 & 1, record->flags >> 2 & 1, record->flags >> 1 & 1, record->flags & 1);
    422 
    423 		if (record->tinfo1)
    424 			fprintf(messages, "Tinfo1: %d\n", record->tinfo1);
    425 
    426 		if (record->tinfo2)
    427 			fprintf(messages, "Tinfo2: %d\n", record->tinfo2);
    428 
    429 		if (record->tinfo3)
    430 			fprintf(messages, "Tinfo3: %d\n", record->tinfo3);
    431 
    432 		if (record->tinfo4)
    433 			fprintf(messages, "Tinfo4: %d\n", record->tinfo4);
    434 
    435 		fprintf(messages, "Tinfos: %s\n", record->tinfos);
    436 		if (record->comments > 0) {
    437 			fprintf(messages, "Comments: ");
    438 			for (int32_t i = 0; i < record->comments; i++)
    439 				fprintf(messages, "%s\n", record->comment_lines[i]);
    440 		}
    441 	}
    442 
    443 	/* Stopping timer */
    444 	clock_gettime(CLOCK_MONOTONIC, &end);
    445 
    446 	timespecsub(&end, &begin, &elapsed);
    447 
    448 	fprintf(messages, "\nProcessed in %f seconds.\n",
    449 	    elapsed.tv_sec + elapsed.tv_nsec / 1E9);
    450 
    451 	ansilove_clean(&ctx);
    452 
    453 	free(record->comment_lines);
    454 	free(record);
    455 
    456 	return EXIT_SUCCESS;
    457 }