ansilove

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

ansilove.c (11648B)


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