ansilove

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

ansilove.c (11604B)


      1 /*
      2  * ansilove.c
      3  * Ansilove 4.1.5
      4  * https://www.ansilove.org
      5  *
      6  * Copyright (c) 2011-2021 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-2021 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 opt;
     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 ((opt = getopt(argc, argv, "b:c:df:him:o:qrR:sSt:v")) != -1) {
    102 		switch (opt) {
    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 	/* if -q flag was not set, default to stdout */
    177 	if (!messages)
    178 		messages = stdout;
    179 
    180 	/* Starting timer */
    181 	clock_gettime(CLOCK_MONOTONIC, &begin);
    182 
    183 	/* let's check the file for a valid SAUCE record */
    184 	struct sauce *record = sauceReadFileName(input);
    185 
    186 	/* record == NULL also means there is no file, we can stop here */
    187 	if (record == NULL) {
    188 		errx(EXIT_FAILURE, "File %s not found.", input);
    189 	} else {
    190 		/* if we find a SAUCE record, update bool flag */
    191 		if (!strcmp(record->ID, SAUCE_ID)) {
    192 			fileHasSAUCE = true;
    193 		}
    194 	}
    195 
    196 	if (!justDisplaySAUCE) {
    197 		/* gather rendering hints from SAUCE */
    198 		if (useSAUCEInfo && fileHasSAUCE) {
    199 			bool usedSAUCE = false;
    200 			if (record->dataType == 1) {
    201 				if (record->fileType == 0 || record->fileType == 1 || record->fileType == 2) {
    202 					options.columns = record->tinfo1;
    203 					if (record->flags & 1)
    204 						options.icecolors = true;
    205 					if ((record->flags & 6) == 4)
    206 						options.bits = 9;
    207 					if ((record->flags & 24) == 8)
    208 						options.dos = true;
    209 					usedSAUCE = true;
    210 				}
    211 				if (record->fileType == 8) {
    212 					options.columns = record->tinfo1;
    213 					usedSAUCE = true;
    214 				}
    215 			}
    216 			if (record->dataType == 5) {
    217 				options.columns = record->tinfo1;
    218 				usedSAUCE = true;
    219 			}
    220 			/* XBIN (dataType == 6) could also use tinfo1 for width, but we trust the XBIN header more */
    221 			/* font info */
    222 			if ((record->dataType == 1 && (record->fileType == 0 || record->fileType == 1 || record->fileType == 2)) || record->dataType == 5 ) {
    223 				if (strcmp(record->tinfos, "IBM VGA") == 0)
    224 					font = "80x25";
    225 
    226 				if (strcmp(record->tinfos, "IBM VGA50") == 0)
    227 					font = "80x50";
    228 
    229 				if (strcmp(record->tinfos, "IBM VGA 437") == 0)
    230 					font = "80x25";
    231 
    232 				if (strcmp(record->tinfos, "IBM VGA50 437") == 0)
    233 					font = "80x50";
    234 
    235 				if (strcmp(record->tinfos, "IBM VGA 775") == 0)
    236 					font = "baltic";
    237 
    238 				if (strcmp(record->tinfos, "IBM VGA50 855") == 0)
    239 					font = "cyrillic";
    240 
    241 				if (strcmp(record->tinfos, "IBM VGA 863") == 0)
    242 					font = "french-canadian";
    243 
    244 				if (strcmp(record->tinfos, "IBM VGA 737") == 0)
    245 					font = "greek";
    246 
    247 				if (strcmp(record->tinfos, "IBM VGA 869") == 0)
    248 					font = "greek-869";
    249 
    250 				if (strcmp(record->tinfos, "IBM VGA 862") == 0)
    251 					font = "hebrew";
    252 
    253 				if (strcmp(record->tinfos, "IBM VGA 861") == 0)
    254 					font = "icelandic";
    255 
    256 				if (strcmp(record->tinfos, "IBM VGA 850") == 0)
    257 					font = "latin1";
    258 
    259 				if (strcmp(record->tinfos, "IBM VGA 852") == 0)
    260 					font = "latin2";
    261 
    262 				if (strcmp(record->tinfos, "IBM VGA 865") == 0)
    263 					font = "nordic";
    264 
    265 				if (strcmp(record->tinfos, "IBM VGA 860") == 0)
    266 					font = "portuguese";
    267 
    268 				if (strcmp(record->tinfos, "IBM VGA 866") == 0)
    269 					font = "russian";
    270 
    271 				if (strcmp(record->tinfos, "IBM VGA 857") == 0)
    272 					font = "turkish";
    273 
    274 				if (strcmp(record->tinfos, "Amiga MicroKnight") == 0)
    275 					font = "microknight";
    276 
    277 				if (strcmp(record->tinfos, "Amiga MicroKnight+") == 0)
    278 					font = "microknight+";
    279 
    280 				if (strcmp(record->tinfos, "Amiga mOsOul") == 0)
    281 					font = "mosoul";
    282 
    283 				if (strcmp(record->tinfos, "Amiga P0T-NOoDLE") == 0)
    284 					font = "pot-noodle";
    285 
    286 				if (strcmp(record->tinfos, "Amiga Topaz 1") == 0)
    287 					font = " topaz500";
    288 
    289 				if (strcmp(record->tinfos, "Amiga Topaz 1+") == 0)
    290 					font = "topaz500+";
    291 
    292 				if (strcmp(record->tinfos, "Amiga Topaz 2") == 0)
    293 					font = "topaz";
    294 
    295 				if (strcmp(record->tinfos, "Amiga Topaz 2+") == 0)
    296 					font = "topaz+";
    297 			}
    298 
    299 			if (usedSAUCE)
    300 				fprintf(messages, "SAUCE info used for rendering hints\n\n");
    301 		}
    302 
    303 		if (font) {
    304 			for (size_t loop = 0; loop < FONTS; loop++) {
    305 				if (!strcmp(fonts[loop], font)) {
    306 					options.font = fontsId[loop];
    307 					break;
    308 				}
    309 			}
    310 		}
    311 
    312 		/* create output file name if output is not specified */
    313 		if (!output) {
    314 			/* appending ".png" extension to output file name */
    315 			if (asprintf(&fileName, "%s%s", input, ".png") == -1)
    316 				errx(EXIT_FAILURE, "Memory allocation error.");
    317 		} else {
    318 			fileName = strdup(output);
    319 
    320 			if (!fileName)
    321 				errx(EXIT_FAILURE, "Memory allocation error.");
    322 		}
    323 
    324 		/* display name of input and output files */
    325 		fprintf(messages, "Input File: %s\n", input);
    326 		fprintf(messages, "Output File: %s\n", fileName);
    327 
    328 		/* get file extension */
    329 		char *fext = strrchr(input, '.');
    330 		fext = fext ? strtolower(strdup(++fext)) : strdup("");
    331 		if (!fext)
    332 			errx(EXIT_FAILURE, "Memory allocation error.");
    333 
    334 		/* check if current file has a .diz extension */
    335 		if (!strcmp(fext, "diz"))
    336 			options.diz = true;
    337 
    338 		if (ansilove_loadfile(&ctx, input) == -1)
    339 			errx(EXIT_FAILURE, "%s", ansilove_error(&ctx));
    340 
    341 		/* adjust the file size if file contains a SAUCE record */
    342 		if (fileHasSAUCE)
    343 			ctx.length -= 129 - (record->comments > 0 ? 5 + 64 * record->comments : 0);
    344 
    345 		int (*loader)(struct ansilove_ctx *, struct ansilove_options *) = NULL;
    346 
    347 		/* if type was specified, attempt to find a loader */
    348 		if (type) {
    349 			for (size_t loop = 0; loop < TYPES; loop++) {
    350 				if (!strcmp(types[loop], type)) {
    351 					loader = loaders[loop];
    352 					filetype = filetypes[loop];
    353 					break;
    354 				}
    355 			}
    356 
    357 			if (!loader)
    358 				errx(EXIT_FAILURE, "Unknown file type.");
    359 		}
    360 
    361 		/* use file extension to find a suitable loader */
    362 		if (!loader) {
    363 			for (size_t loop = 0; loop < TYPES; loop++) {
    364 				if (!strcmp(types[loop], fext)) {
    365 					loader = loaders[loop];
    366 					filetype = filetypes[loop];
    367 					break;
    368 				}
    369 			}
    370 		}
    371 
    372 		/* default to ANSI if file extension is unknown */
    373 		if (!loader) {
    374 			loader = ansilove_ansi;
    375 			filetype = ANSILOVE_FILETYPE_ANS;
    376 		}
    377 
    378 		if (loader(&ctx, &options) == -1)
    379 			errx(EXIT_FAILURE, "%s", ansilove_error(&ctx));
    380 
    381 		/* create the output file */
    382 		if (ansilove_savefile(&ctx, fileName) == -1)
    383 			errx(EXIT_FAILURE, "%s", ansilove_error(&ctx));
    384 
    385 		/* gather information and report to the command line */
    386 		switch(filetype) {
    387 		case ANSILOVE_FILETYPE_ANS:
    388 		case ANSILOVE_FILETYPE_BIN:
    389 			if (options.icecolors)
    390 				fprintf(messages, "iCE Colors: enabled\n");
    391 
    392 			/* FALLTHROUGH */
    393 		case ANSILOVE_FILETYPE_PCB:
    394 		case ANSILOVE_FILETYPE_TND:
    395 			fprintf(messages, "Font: %s\n", font ? font : "80x25");
    396 			fprintf(messages, "Bits: %d\n", options.bits);
    397 			fprintf(messages, "Columns: %d\n", options.columns);
    398 		}
    399 
    400 		if (options.scale_factor)
    401 			fprintf(messages, "Scale factor: %d\n", options.scale_factor);
    402 
    403 		free(fileName);
    404 		free(fext);
    405 	}
    406 
    407 	/* either display SAUCE or tell us if there is no record */
    408 	if (!fileHasSAUCE) {
    409 		fprintf(messages, "\nFile %s does not have a SAUCE record.\n", input);
    410 	} else {
    411 		fprintf(messages, "\nId: %s v%s\n", record->ID, record->version);
    412 		fprintf(messages, "Title: %s\n", record->title);
    413 		fprintf(messages, "Author: %s\n", record->author);
    414 		fprintf(messages, "Group: %s\n", record->group);
    415 		fprintf(messages, "Date: %s\n", record->date);
    416 		fprintf(messages, "Datatype: %d\n", record->dataType);
    417 		fprintf(messages, "Filetype: %d\n", record->fileType);
    418 
    419 		if (record->flags)
    420 			fprintf(messages, "Flags: 0b%d%d%d%d%d%d%d%d\n",
    421 				record->flags >> 7,
    422 				record->flags >> 6 & 1,
    423 				record->flags >> 5 & 1,
    424 				record->flags >> 4 & 1,
    425 				record->flags >> 3 & 1,
    426 				record->flags >> 2 & 1,
    427 				record->flags >> 1 & 1,
    428 				record->flags & 1);
    429 
    430 		if (record->tinfo1)
    431 			fprintf(messages, "Tinfo1: %d\n", record->tinfo1);
    432 
    433 		if (record->tinfo2)
    434 			fprintf(messages, "Tinfo2: %d\n", record->tinfo2);
    435 
    436 		if (record->tinfo3)
    437 			fprintf(messages, "Tinfo3: %d\n", record->tinfo3);
    438 
    439 		if (record->tinfo4)
    440 			fprintf(messages, "Tinfo4: %d\n", record->tinfo4);
    441 
    442 		fprintf(messages, "Tinfos: %s\n", record->tinfos);
    443 		if (record->comments > 0) {
    444 			fprintf(messages, "Comments: ");
    445 			for (int32_t i = 0; i < record->comments; i++)
    446 				fprintf(messages, "%s\n", record->comment_lines[i]);
    447 		}
    448 	}
    449 
    450 	/* Stopping timer */
    451 	clock_gettime(CLOCK_MONOTONIC, &end);
    452 
    453 	timespecsub(&end, &begin, &elapsed);
    454 
    455 	fprintf(messages, "\nProcessed in %f seconds.\n",
    456 	    elapsed.tv_sec + elapsed.tv_nsec / 1E9);
    457 
    458 	ansilove_clean(&ctx);
    459 
    460 	free(record->comment_lines);
    461 	free(record);
    462 
    463 	return EXIT_SUCCESS;
    464 }