ansilove.c (11648B)
1 /* 2 * ansilove.c 3 * Ansilove 4.1.6 4 * https://www.ansilove.org 5 * 6 * Copyright (c) 2011-2022 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-2022 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 }