shithub: rgbds

Download patch

ref: 91071009a8892793ff2a434de347d2d57cf313dd
parent: 1da884db15b5fc84159b2dd6c1b12f0f02eb396c
author: ISSOtm <eldredhabert0@gmail.com>
date: Sat Apr 9 11:58:25 EDT 2022

Implement some external palette specs

PSP, ACT, and ACO are complete

--- a/include/gfx/pal_spec.hpp
+++ b/include/gfx/pal_spec.hpp
@@ -9,8 +9,7 @@
 #ifndef RGBDS_GFX_PAL_SPEC_HPP
 #define RGBDS_GFX_PAL_SPEC_HPP
 
-#include <string_view>
-
 void parseInlinePalSpec(char const * const arg);
+void parseExternalPalSpec(char const *arg);
 
 #endif /* RGBDS_GFX_PAL_SPEC_HPP */
--- a/src/gfx/main.cpp
+++ b/src/gfx/main.cpp
@@ -21,6 +21,7 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <string_view>
 
 #include "extern/getopt.h"
 #include "platform.h"
@@ -33,6 +34,7 @@
 using namespace std::literals::string_view_literals;
 
 Options options;
+char const *externalPalSpec = nullptr;
 static uintmax_t nbErrors;
 
 void warning(char const *fmt, ...) {
@@ -222,21 +224,6 @@
 	arg += strcspn(arg, " \t");
 }
 
-static void parsePaletteSpec(char const *arg) {
-	if (arg[0] == '#') {
-		options.palSpecType = Options::EXPLICIT;
-		parseInlinePalSpec(arg);
-	} else if (strcasecmp(arg, "embedded") == 0) {
-		// Use PLTE, error out if missing
-		options.palSpecType = Options::EMBEDDED;
-	} else {
-		// `fmt:path`, parse the file according to the given format
-		// TODO: split both parts, error out if malformed or file not found
-		options.palSpecType = Options::EXPLICIT;
-		// TODO
-	}
-}
-
 static void registerInput(char const *arg) {
 	if (!options.input.empty()) {
 		fprintf(stderr,
@@ -375,7 +362,20 @@
 			options.useColorCurve = true;
 			break;
 		case 'c':
-			parsePaletteSpec(musl_optarg);
+			if (musl_optarg[0] == '#') {
+				options.palSpecType = Options::EXPLICIT;
+				parseInlinePalSpec(musl_optarg);
+			} else if (strcasecmp(musl_optarg, "embedded") == 0) {
+				// Use PLTE, error out if missing
+				options.palSpecType = Options::EMBEDDED;
+			} else {
+				options.palSpecType = Options::EXPLICIT;
+				// Can't parse the file yet, as "flat" color collections need to know the palette
+				// size to be split; thus, we defer that
+				// TODO: this does not validate the `fmt` part of any external spec but the last
+				// one, but I guess that's okay
+				externalPalSpec = musl_optarg;
+			}
 			break;
 		case 'D':
 			warning("Ignoring retired option `-D`");
@@ -611,6 +611,11 @@
 	autoOutPath(autoTilemap, options.tilemap, ".tilemap");
 	autoOutPath(autoPalettes, options.palettes, ".pal");
 
+	// Execute deferred external pal spec parsing, now that all other params are known
+	if (externalPalSpec) {
+		parseExternalPalSpec(externalPalSpec);
+	}
+
 	if (options.verbosity >= Options::VERB_CFG) {
 		fprintf(stderr, "rgbgfx %s\n", get_package_version_string());
 
@@ -706,7 +711,8 @@
 
 	// Do not do anything if option parsing went wrong
 	if (nbErrors) {
-		fprintf(stderr, "Conversion aborted after %ju error%s\n", nbErrors, nbErrors == 1 ? "" : "s");
+		fprintf(stderr, "Conversion aborted after %ju error%s\n", nbErrors,
+		        nbErrors == 1 ? "" : "s");
 		return 1;
 	}
 
@@ -717,7 +723,8 @@
 	}
 
 	if (nbErrors) {
-		fprintf(stderr, "Conversion aborted after %ju error%s\n", nbErrors, nbErrors == 1 ? "" : "s");
+		fprintf(stderr, "Conversion aborted after %ju error%s\n", nbErrors,
+		        nbErrors == 1 ? "" : "s");
 		return 1;
 	}
 	return 0;
--- a/src/gfx/pal_spec.cpp
+++ b/src/gfx/pal_spec.cpp
@@ -10,8 +10,22 @@
 
 #include <algorithm>
 #include <cassert>
+#include <cinttypes>
+#include <climits>
+#include <cstdint>
 #include <cstdio>
+#include <cstring>
+#include <fstream>
+#include <ostream>
+#include <streambuf>
+#include <string>
+#include <string_view>
+#include <tuple>
+#include <type_traits>
+#include <unordered_map>
 
+#include "platform.h"
+
 #include "gfx/main.hpp"
 
 using namespace std::string_view_literals;
@@ -37,6 +51,11 @@
 	return toHex(c, c);
 }
 
+template<typename Str> // Should be std::string or std::string_view
+static void skipWhitespace(Str const &str, typename Str::size_type &pos) {
+	pos = std::min(str.find_first_not_of(" \t", pos), str.length());
+}
+
 void parseInlinePalSpec(char const * const rawArg) {
 	// List of #rrggbb/#rgb colors, comma-separated, palettes are separated by colons
 
@@ -63,13 +82,8 @@
 		putc('\n', stderr);
 	};
 
-	auto skipWhitespace = [&arg](size_type &pos) {
-		pos = std::min(arg.find_first_not_of(" \t", pos), arg.length());
-	};
-
 	options.palSpec.clear();
-	options.palSpec
-	    .emplace_back(); // Not default-initialized, but value-initialized, so we get zeros
+	options.palSpec.emplace_back(); // Value-initialized, not default-init'd, so we get zeros
 
 	size_type n = 0; // Index into the argument
 	// TODO: store max `nbColors` ever reached, and compare against palette size later
@@ -98,7 +112,7 @@
 		n = pos;
 
 		// Skip whitespace, if any
-		skipWhitespace(n);
+		skipWhitespace(arg, n);
 
 		// Skip comma/colon, or end
 		if (n == arg.length()) {
@@ -111,7 +125,7 @@
 			++nbColors;
 
 			// A trailing comma may be followed by a colon
-			skipWhitespace(n);
+			skipWhitespace(arg, n);
 			if (n == arg.length()) {
 				break;
 			} else if (arg[n] != ':') {
@@ -125,7 +139,7 @@
 
 		case ':':
 			++n;
-			skipWhitespace(n);
+			skipWhitespace(arg, n);
 
 			nbColors = 0; // Start a new palette
 			// Avoid creating a spurious empty palette
@@ -148,4 +162,290 @@
 			return;
 		}
 	}
+}
+
+/**
+ * Tries to read some magic bytes from the provided `file`.
+ * Returns whether the magic was correctly read.
+ */
+template<size_t n>
+static bool readMagic(std::filebuf &file, char const *magic) {
+	assert(strlen(magic) == n);
+
+	char magicBuf[n];
+	return file.sgetn(magicBuf, n) == n && memcmp(magicBuf, magic, n);
+}
+
+// Like `readMagic`, but automatically determines the size from the string literal's length.
+// Don't worry if you make a mistake, an `assert`'s got your back!
+#define READ_MAGIC(file, magic) \
+	readMagic<sizeof(magic) - 1>(file, magic) // Don't count the terminator
+
+template<typename T, typename U>
+static T readBE(U const *bytes) {
+	T val = 0;
+	for (size_t i = 0; i < sizeof(val); ++i) {
+		val = val << 8 | static_cast<uint8_t>(bytes[i]);
+	}
+	return val;
+}
+
+/**
+ * **Appends** the first line read from `file` to the end of the provided `buffer`.
+ */
+static void readLine(std::filebuf &file, std::string &buffer) {
+	// TODO: maybe this can be optimized to bulk reads?
+	for (;;) {
+		auto c = file.sbumpc();
+		if (c == std::filebuf::traits_type::eof()) {
+			return;
+		}
+		if (c == '\n') {
+			// Discard a trailing CRLF
+			if (!buffer.empty() && buffer.back() == '\r') {
+				buffer.pop_back();
+			}
+			return;
+		}
+
+		buffer.push_back(c);
+	}
+}
+
+// FIXME: Normally we'd use `std::from_chars`, but that's not available with GCC 7
+/**
+ * Parses the initial part of a string_view, advancing the "read index" as it does
+ */
+static uint16_t parseDec(std::string const &str, std::string::size_type &n) {
+	uint32_t value = 0; // Use a larger type to handle overflow more easily
+	for (auto end = std::min(str.length(), str.find_first_not_of("0123456789", n)); n < end; ++n) {
+		value = std::min<uint32_t>(value * 10 + (str[n] - '0'), UINT16_MAX);
+	}
+
+	return value;
+}
+
+static void parsePSPFile(std::filebuf &file) {
+	// https://www.selapa.net/swatches/colors/fileformats.php#psp_pal
+
+	std::string line;
+	readLine(file, line);
+	if (line != "JASC-PAL") {
+		error("Palette file does not appear to be a PSP palette file");
+		return;
+	}
+
+	line.clear();
+	readLine(file, line);
+	if (line != "0100") {
+		error("Unsupported PSP palette file version \"%s\"", line.c_str());
+		return;
+	}
+
+	line.clear();
+	readLine(file, line);
+	std::string::size_type n = 0;
+	uint16_t nbColors = parseDec(line, n);
+	if (n != line.length()) {
+		error("Invalid \"number of colors\" line in PSP file (%s)", line.c_str());
+		return;
+	}
+
+	if (nbColors > options.nbColorsPerPal * options.nbPalettes) {
+		warning("PSP file contains %" PRIu16 " colors, but there can only be %" PRIu16
+		        "; ignoring extra",
+		        nbColors, options.nbColorsPerPal * options.nbPalettes);
+		nbColors = options.nbColorsPerPal * options.nbPalettes;
+	}
+
+	options.palSpec.clear();
+
+	for (uint16_t i = 0; i < nbColors; ++i) {
+		line.clear();
+		readLine(file, line);
+		n = 0;
+
+		// TODO: parse R G B
+		uint8_t r = parseDec(line, n);
+		skipWhitespace(line, n);
+		if (n == line.length()) {
+			error("Failed to parse color #%" PRIu16 " (\"%s\"): missing green component", i + 1,
+			      line.c_str());
+			return;
+		}
+		uint8_t g = parseDec(line, n);
+		if (n == line.length()) {
+			error("Failed to parse color #%" PRIu16 " (\"%s\"): missing green component", i + 1,
+			      line.c_str());
+			return;
+		}
+		skipWhitespace(line, n);
+		uint8_t b = parseDec(line, n);
+		if (n != line.length()) {
+			error("Failed to parse color #%" PRIu16
+			      " (\"%s\"): trailing characters after blue component",
+			      i + 1, line.c_str());
+			return;
+		}
+
+		if (i % options.nbColorsPerPal == 0) {
+			options.palSpec.emplace_back();
+		}
+		options.palSpec.back()[i % options.nbColorsPerPal] = Rgba(r, g, b, 0xFF);
+	}
+}
+
+void parseACTFile(std::filebuf &file) {
+	// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1070626
+
+	std::array<char, 772> buf;
+	auto len = file.sgetn(buf.data(), buf.size());
+
+	uint16_t nbColors = 256;
+	if (len == 772) {
+		nbColors = readBE<uint16_t>(&buf[768]);
+		// TODO: apparently there is a "transparent color index"? What?
+		if (nbColors > 256 || nbColors == 0) {
+			error("Invalid number of colors in ACT file (%" PRIu16 ")", nbColors);
+			return;
+		}
+	} else if (len != 768) {
+		error("Invalid file size for ACT file (expected 768 or 772 bytes, got %zu", len);
+		return;
+	}
+
+	if (nbColors > options.nbColorsPerPal * options.nbPalettes) {
+		warning("ACT file contains %" PRIu16 " colors, but there can only be %" PRIu16
+		        "; ignoring extra",
+		        nbColors, options.nbColorsPerPal * options.nbPalettes);
+		nbColors = options.nbColorsPerPal * options.nbPalettes;
+	}
+
+	options.palSpec.clear();
+	options.palSpec.emplace_back();
+
+	char const *ptr = buf.data();
+	size_t colorIdx = 0;
+	for (uint16_t i = 0; i < nbColors; ++i) {
+		Rgba &color = options.palSpec.back()[colorIdx];
+		color = Rgba(ptr[0], ptr[1], ptr[2], 0xFF);
+
+		ptr += 3;
+		++colorIdx;
+		if (colorIdx == options.nbColorsPerPal) {
+			options.palSpec.emplace_back();
+			colorIdx = 0;
+		}
+	}
+
+	// Remove the spurious empty palette if there is one
+	if (colorIdx == 0) {
+		options.palSpec.pop_back();
+	}
+}
+
+void parseACOFile(std::filebuf &file) {
+	// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1055819
+	// http://www.nomodes.com/aco.html
+
+	char buf[10];
+
+	if (file.sgetn(buf, 2) != 2) {
+		error("Couldn't read ACO file version");
+		return;
+	}
+	if (readBE<uint16_t>(buf) != 1) {
+		error("Palette file does not appear to be an ACO file");
+		return;
+	}
+
+	if (file.sgetn(buf, 2) != 2) {
+		error("Couldn't read number of colors in palette file");
+		return;
+	}
+	uint16_t nbColors = readBE<uint16_t>(buf);
+
+	if (nbColors > options.nbColorsPerPal * options.nbPalettes) {
+		warning("ACO file contains %" PRIu16 " colors, but there can only be %" PRIu16
+		        "; ignoring extra",
+		        nbColors, options.nbColorsPerPal * options.nbPalettes);
+		nbColors = options.nbColorsPerPal * options.nbPalettes;
+	}
+
+	options.palSpec.clear();
+
+	for (uint16_t i = 0; i < nbColors; ++i) {
+		if (file.sgetn(buf, 10) != 10) {
+			error("Failed to read color #%" PRIu16 " from palette file", i + 1);
+			return;
+		}
+
+		if (i % options.nbColorsPerPal == 0) {
+			options.palSpec.emplace_back();
+		}
+
+		Rgba &color = options.palSpec.back()[i % options.nbColorsPerPal];
+		uint16_t colorType = readBE<uint16_t>(buf);
+		switch (colorType) {
+		case 0: // RGB
+			color = Rgba(buf[0], buf[2], buf[4], 0xFF);
+			break;
+		case 1: // HSB
+			error("Unsupported color type (HSB) for ACO file");
+			return;
+		case 2: // CMYK
+			error("Unsupported color type (CMYK) for ACO file");
+			return;
+		case 7: // Lab
+			error("Unsupported color type (lab) for ACO file");
+			return;
+		case 8: // Grayscale
+			error("Unsupported color type (grayscale) for ACO file");
+			return;
+		default:
+			error("Unknown color type (%" PRIu16 ") for ACO file", colorType);
+			return;
+		}
+	}
+
+	// TODO: maybe scan the v2 data instead (if present)
+	// `codecvt` can be used to convert from UTF-16 to UTF-8
+}
+
+void parseExternalPalSpec(char const *arg) {
+	// `fmt:path`, parse the file according to the given format
+
+	// Split both parts, error out if malformed
+	char const *ptr = strchr(arg, ':');
+	if (ptr == nullptr) {
+		error("External palette spec must have format `fmt:path` (missing colon)");
+		return;
+	}
+	char const *path = ptr + 1;
+
+	static std::array parsers{
+	    std::tuple{"PSP", &parsePSPFile, std::ios::in    },
+	    std::tuple{"ACT", &parseACTFile, std::ios::binary},
+	    std::tuple{"ACO", &parseACOFile, std::ios::binary},
+	};
+
+	auto iter = std::find_if(parsers.begin(), parsers.end(),
+	                         [&arg, &ptr](decltype(parsers)::value_type const &parser) {
+		                         return strncasecmp(arg, std::get<0>(parser), ptr - arg) == 0;
+	                         });
+	if (iter == parsers.end()) {
+		error("Unknown external palette format \"%.*s\"",
+		      static_cast<int>(std::min(ptr - arg, static_cast<decltype(ptr - arg)>(INT_MAX))),
+		      arg);
+		return;
+	}
+
+	std::filebuf file;
+	// Some parsers read the file in text mode, others in binary mode
+	if (!file.open(path, std::ios::in | std::get<2>(*iter))) {
+		error("Failed to open palette file \"%s\"", path);
+		return;
+	}
+
+	std::get<1> (*iter)(file);
 }