text: #include <stdio.h>
  #include <stdlib.h>
  #include <stdbool.h>
  #include <getopt.h>
  #include <string.h>
  #include <stdint.h>
  
  #include "common.h"
  
  static void usage(void) {
  	fprintf(stderr, "Usage: gfx [[--trim-whitespace] | [--trim-trailing]] [--remove-whitespace] [--interleave] "
  	                "[--remove-duplicates [--keep-whitespace]] [--remove-xflip] [--remove-yflip] [--png filename] "
  	                "[-d depth] [-h] [-o outfile] infile\n");
  }
  
  static void error(char *message) {
  	fputs(message, stderr);
  	fputs("\n", stderr);
  }
  
  struct Options {
  	int trim_whitespace;
  	int trim_trailing;
  	int remove_whitespace;
  	int help;
  	char *outfile;
  	int depth;
  	int interleave;
  	int remove_duplicates;
  	int keep_whitespace;
  	int remove_xflip;
  	int remove_yflip;
  	char *png_file;
  };
  
  struct Options Options = {
  	.depth = 2,
  };
  
  void get_args(int argc, char *argv[]) {
  	struct option long_options[] = {
  		{"remove-whitespace", no_argument, &Options.remove_whitespace, 1},
  		{"trim-whitespace", no_argument, &Options.trim_whitespace, 1},
  		{"trim-trailing", no_argument, &Options.trim_trailing, 1},
  		{"interleave", no_argument, &Options.interleave, 1},
  		{"remove-duplicates", no_argument, &Options.remove_duplicates, 1},
  		{"keep-whitespace", no_argument, &Options.keep_whitespace, 1},
  		{"remove-xflip", no_argument, &Options.remove_xflip, 1},
  		{"remove-yflip", no_argument, &Options.remove_yflip, 1},
  		{"png", required_argument, 0, 'p'},
  		{"depth", required_argument, 0, 'd'},
  		{"help", no_argument, 0, 'h'},
  		{0}
  	};
  	for (int opt = 0; opt != -1;) {
  		switch (opt = getopt_long(argc, argv, "ho:d:p:", long_options)) {
  		case 'h':
  			Options.help = true;
  			break;
  		case 'o':
  			Options.outfile = optarg;
  			break;
  		case 'd':
  			Options.depth = strtoul(optarg, NULL, 0);
  			break;
  		case 'p':
  			Options.png_file = optarg;
  			break;
  		case 0:
  		case -1:
  			break;
  		default:
  			usage();
  			exit(1);
  			break;
  		}
  	}
  }
  
  struct Graphic {
  	int size;
  	uint8_t *data;
  };
  
  bool is_whitespace(uint8_t *tile, int tile_size) {
  	uint8_t WHITESPACE = 0;
  	for (int i = 0; i < tile_size; i++) {
  		if (tile[i] != WHITESPACE) {
  			return false;
  		}
  	}
  	return true;
  }
  
  /**
   * is_solid_color
   * Check if a tile is solid color, i.e. each
   * bitplane is filled with the same value.
   * Different bitplanes can have different values.
   * 
   * Returns true if solid color, else false.
   */
  bool is_solid_color(uint8_t *tile, int depth, int tile_size) {
  	
  	const uint8_t ONES = 0xff;
  	const uint8_t ZEROS = 0x00;
  	
  	for (int i = 0; i < depth; i++) {
  		if (tile[i] != ONES && tile[i] != ZEROS) {
  			return false;
  		}
  	}
  	return !memcmp(&tile[0], &tile[depth], tile_size - depth);
  }
  
  void trim_whitespace(struct Graphic *graphic) {
  	int tile_size = Options.depth * 8;
  	for (int i = graphic->size - tile_size; i > 0; i -= tile_size) {
  		if (is_whitespace(&graphic->data[i], tile_size)) {
  			graphic->size = i;
  		} else {
  			break;
  		}
  	}
  }
  
  void trim_trailing(struct Graphic *graphic) {
  	int tile_size = Options.depth * 8;
  	int last_tile = graphic->size - tile_size;
  	if (!is_solid_color(&graphic->data[last_tile], Options.depth, tile_size)) {
  		return;
  	}
  	for (int i = graphic->size -  2 * tile_size; i > 0; i -= tile_size) {
  		if (!memcmp(&graphic->data[i], &graphic->data[last_tile], tile_size)) {
  			graphic->size = i;
  		} else {
  			break;
  		}
  	}
  }
  
  void remove_whitespace(struct Graphic *graphic) {
  	int tile_size = Options.depth * 8;
  	if (Options.interleave) tile_size *= 2;
  	int i = 0;
  	for (int j = 0; i < graphic->size && j < graphic->size; i += tile_size, j += tile_size) {
  		while (is_whitespace(&graphic->data[j], tile_size)) {
  			j += tile_size;
  		}
  		if (j >= graphic->size) {
  			break;
  		}
  		if (j > i) {
  			memcpy(&graphic->data[i], &graphic->data[j], tile_size);
  		}
  	}
  	graphic->size = i;
  }
  
  bool tile_exists(uint8_t *tile, uint8_t *tiles, int tile_size, int num_tiles) {
  	for (int i = 0; i < num_tiles; i++) {
  		bool match = true;
  		for (int j = 0; j < tile_size; j++) {
  			if (tile[j] != tiles[i * tile_size + j]) {
  				match = false;
  			}
  		}
  		if (match) {
  			return true;
  		}
  	}
  	return false;
  }
  
  void remove_duplicates(struct Graphic *graphic) {
  	int tile_size = Options.depth * 8;
  	if (Options.interleave) tile_size *= 2;
  	int num_tiles = 0;
  	for (int i = 0, j = 0; i < graphic->size && j < graphic->size; i += tile_size, j += tile_size) {
  		while (tile_exists(&graphic->data[j], graphic->data, tile_size, num_tiles)) {
  			if (Options.keep_whitespace && is_whitespace(&graphic->data[j], tile_size)) {
  				break;
  			}
  			j += tile_size;
  		}
  		if (j >= graphic->size) {
  			break;
  		}
  		if (j > i) {
  			memcpy(&graphic->data[i], &graphic->data[j], tile_size);
  		}
  		num_tiles++;
  	}
  	graphic->size = num_tiles * tile_size;
  }
  
  bool flip_exists(uint8_t *tile, uint8_t *tiles, int tile_size, int num_tiles, bool xflip, bool yflip) {
  	uint8_t *flip = calloc(tile_size, 1);
  	int half_size = tile_size / 2;
  	for (int i = 0; i < tile_size; i++) {
  		int byte = i;
  		if (yflip) {
  			byte = tile_size - 1 - (i ^ 1);
  			if (Options.interleave && i < half_size) {
  				byte = half_size - 1 - (i ^ 1);
  			}
  		}
  		if (xflip) {
  			for (int bit = 0; bit < 8; bit++) {
  				flip[byte] |= ((tile[i] >> bit) & 1) << (7 - bit);
  			}
  		} else {
  			flip[byte] = tile[i];
  		}
  	}
  	if (tile_exists(flip, tiles, tile_size, num_tiles)) {
  		return true;
  	}
  	return false;
  }
  
  void remove_flip(struct Graphic *graphic, bool xflip, bool yflip) {
  	int tile_size = Options.depth * 8;
  	if (Options.interleave) tile_size *= 2;
  	int num_tiles = 0;
  	for (int i = 0, j = 0; i < graphic->size && j < graphic->size; i += tile_size, j += tile_size) {
  		while (flip_exists(&graphic->data[j], graphic->data, tile_size, num_tiles, xflip, yflip)) {
  			if (Options.keep_whitespace && is_whitespace(&graphic->data[j], tile_size)) {
  				break;
  			}
  			j += tile_size;
  		}
  		if (j >= graphic->size) {
  			break;
  		}
  		if (j > i) {
  			memcpy(&graphic->data[i], &graphic->data[j], tile_size);
  		}
  		num_tiles++;
  	}
  	graphic->size = num_tiles * tile_size;
  }
  
  void interleave(struct Graphic *graphic, int width) {
  	int tile_size = Options.depth * 8;
  	int width_tiles = width / 8;
  	int num_tiles = graphic->size / tile_size;
  	uint8_t *interleaved = malloc(graphic->size);
  	for (int i = 0; i < num_tiles; i++) {
  		int tile = i * 2;
  		int row = i / width_tiles;
  		tile -= width_tiles * row;
  		if (row % 2) {
  			tile -= width_tiles;
  			tile += 1;
  		}
  		memcpy(&interleaved[tile * tile_size], &graphic->data[i * tile_size], tile_size);
  	}
  	graphic->size = num_tiles * tile_size;
  	memcpy(graphic->data, interleaved, graphic->size);
  	free(interleaved);
  }
  
  int png_get_width(char *filename) {
  	FILE *f = fopen_verbose(filename, "rb");
  	if (!f) {
  		exit(1);
  	}
  
  	const int OFFSET_WIDTH = 16;
  	uint8_t bytes[4];
  	fseek(f, OFFSET_WIDTH, SEEK_SET);
  	size_t size = 4;
  	size_t result = fread(bytes, 1, size, f);
  	fclose(f);
  	if (result != size) {
  		fprintf(stderr, "Could not read file at offset 0x%x: \"%s\"\n", OFFSET_WIDTH, filename);
  		exit(1);
  	}
  
  	int width = 0;
  	for (int i = 0; i < 4; i++) {
  		width |= bytes[i] << (8 * (3 - i));
  	}
  	return width;
  }
  
  
  int main(int argc, char *argv[]) {
  	get_args(argc, argv);
  	argc -= optind;
  	argv += optind;
  	if (Options.help) {
  		usage();
  		return 0;
  	}
  	if (argc < 1) {
  		usage();
  		exit(1);
  	}
  	char *infile = argv[0];
  	struct Graphic graphic;
  	graphic.data = read_u8(infile, &graphic.size);
  	if (Options.trim_whitespace) {
  		trim_whitespace(&graphic);
  	} else if (Options.trim_trailing) {
  		trim_trailing(&graphic);
  	}
  	if (Options.interleave) {
  		if (!Options.png_file) {
  			error("interleave: need --png to infer dimensions");
  			usage();
  			exit(1);
  		}
  		int width = png_get_width(Options.png_file);
  		interleave(&graphic, width);
  	}
  	if (Options.remove_duplicates) {
  		remove_duplicates(&graphic);
  	}
  	if (Options.remove_xflip) {
  		remove_flip(&graphic, true, false);
  	}
  	if (Options.remove_yflip) {
  		remove_flip(&graphic, false, true);
  	}
  	if (Options.remove_xflip && Options.remove_yflip) {
  		remove_flip(&graphic, true, true);
  	}
  	if (Options.remove_whitespace) {
  		remove_whitespace(&graphic);
  	}
  	if (Options.outfile) {
  		write_u8(Options.outfile, graphic.data, graphic.size);
  	}
  	free(graphic.data);
  	return 0;
  }
  