shithub: rgbds

Download patch

ref: ddb1d0b6aa6fb36081e2cc24176c0e6e77bcbe44
parent: 08545643cfb75b02a23e1af6b853f1432dd422f0
author: Rangi <35663410+Rangi42@users.noreply.github.com>
date: Sat Oct 1 00:46:13 EDT 2022

Parse GPL palettes, and fix PSP palette parsing (#1080)

Addresses one item of #1065

--- a/Makefile
+++ b/Makefile
@@ -9,6 +9,8 @@
 .SUFFIXES:
 .SUFFIXES: .h .y .c .cpp .o
 
+.PHONY: all clean install checkcodebase checkpatch checkdiff develop debug mingw32 mingw64 wine-shim dist
+
 # User-defined variables
 
 Q		:= @
@@ -246,6 +248,13 @@
 		-fsanitize=signed-integer-overflow -fsanitize=bounds \
 		-fsanitize=object-size -fsanitize=bool -fsanitize=enum \
 		-fsanitize=alignment -fsanitize=null -fsanitize=address" \
+		CFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls" \
+		CXXFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls"
+
+# This target is used during development in order to more easily debug with gdb.
+
+debug:
+	$Qenv ${MAKE} \
 		CFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls" \
 		CXXFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls"
 
--- a/man/rgbgfx.1
+++ b/man/rgbgfx.1
@@ -380,6 +380,8 @@
 .Lk https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1055819 Adobe Photoshop color swatch .
 .It Sy psp
 .Lk https://www.selapa.net/swatches/colors/fileformats.php#psp_pal Paint Shop Pro palette .
+.It Sy gpl
+.Lk https://docs.gimp.org/2.10/en/gimp-concepts-palettes.html GIMP palette .
 .El
 .Pp
 If you wish for another format to be supported, please open an issue (see
--- a/src/gfx/pal_spec.cpp
+++ b/src/gfx/pal_spec.cpp
@@ -16,6 +16,8 @@
 #include <cstdio>
 #include <cstring>
 #include <fstream>
+#include <limits>
+#include <optional>
 #include <ostream>
 #include <streambuf>
 #include <string>
@@ -226,15 +228,54 @@
 /*
  * 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
+template<typename U> // Should be uint*_t
+static std::optional<U> parseDec(std::string const &str, std::string::size_type &n) {
+	std::string::size_type start = n;
+
+	uintmax_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);
+		value = std::min(value * 10 + (str[n] - '0'), (uintmax_t)std::numeric_limits<U>::max);
 	}
 
-	return value;
+	return n > start ? std::optional<U>{value} : std::nullopt;
 }
 
+static std::optional<Rgba> parseColor(std::string const &str, std::string::size_type &n,
+                                      uint16_t i) {
+	std::optional<uint8_t> r = parseDec<uint8_t>(str, n);
+	if (!r) {
+		error("Failed to parse color #%" PRIu16 " (\"%s\"): invalid red component", i + 1,
+		      str.c_str());
+		return std::nullopt;
+	}
+	skipWhitespace(str, n);
+	if (n == str.length()) {
+		error("Failed to parse color #%" PRIu16 " (\"%s\"): missing green component", i + 1,
+		      str.c_str());
+		return std::nullopt;
+	}
+	std::optional<uint8_t> g = parseDec<uint8_t>(str, n);
+	if (!g) {
+		error("Failed to parse color #%" PRIu16 " (\"%s\"): invalid green component", i + 1,
+		      str.c_str());
+		return std::nullopt;
+	}
+	skipWhitespace(str, n);
+	if (n == str.length()) {
+		error("Failed to parse color #%" PRIu16 " (\"%s\"): missing blue component", i + 1,
+		      str.c_str());
+		return std::nullopt;
+	}
+	std::optional<uint8_t> b = parseDec<uint8_t>(str, n);
+	if (!b) {
+		error("Failed to parse color #%" PRIu16 " (\"%s\"): invalid blue component", i + 1,
+		      str.c_str());
+		return std::nullopt;
+	}
+
+	return std::optional<Rgba>{Rgba(*r, *g, *b, 0xFF)};
+}
+
 static void parsePSPFile(std::filebuf &file) {
 	// https://www.selapa.net/swatches/colors/fileformats.php#psp_pal
 
@@ -255,41 +296,30 @@
 	line.clear();
 	readLine(file, line);
 	std::string::size_type n = 0;
-	uint16_t nbColors = parseDec(line, n);
-	if (n != line.length()) {
+	std::optional<uint16_t> nbColors = parseDec<uint16_t>(line, n);
+	if (!nbColors || n != line.length()) {
 		error("Invalid \"number of colors\" line in PSP file (%s)", line.c_str());
 		return;
 	}
 
-	if (nbColors > options.nbColorsPerPal * options.nbPalettes) {
+	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);
 		nbColors = options.nbColorsPerPal * options.nbPalettes;
 	}
 
 	options.palSpec.clear();
 
-	for (uint16_t i = 0; i < nbColors; ++i) {
+	for (uint16_t i = 0; i < *nbColors; ++i) {
 		line.clear();
 		readLine(file, line);
-		n = 0;
 
-		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());
+		n = 0;
+		std::optional<Rgba> color = parseColor(line, n, i + 1);
+		if (!color) {
 			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",
@@ -300,10 +330,58 @@
 		if (i % options.nbColorsPerPal == 0) {
 			options.palSpec.emplace_back();
 		}
-		options.palSpec.back()[i % options.nbColorsPerPal] = Rgba(r, g, b, 0xFF);
+		options.palSpec.back()[i % options.nbColorsPerPal] = *color;
 	}
 }
 
+static void parseGPLFile(std::filebuf &file) {
+	// https://gitlab.gnome.org/GNOME/gimp/-/blob/gimp-2-10/app/core/gimppalette-load.c#L39
+
+	std::string line;
+	readLine(file, line);
+	// FIXME: C++20 will allow `!line.starts_with` instead of `line.rfind` with 0
+	if (line.rfind("GIMP Palette", 0)) {
+		error("Palette file does not appear to be a GPL palette file");
+		return;
+	}
+
+	uint16_t nbColors = 0;
+	uint16_t maxNbColors = options.nbColorsPerPal * options.nbPalettes;
+
+	for (;;) {
+		line.clear();
+		readLine(file, line);
+		if (!line.length()) {
+			break;
+		}
+
+		// FIXME: C++20 will allow `line.starts_with` instead of `!line.rfind` with 0
+		if (!line.rfind("#", 0) | !line.rfind("Name:", 0) || !line.rfind("Column:", 0)) {
+			continue;
+		}
+
+		std::string::size_type n = 0;
+		std::optional<Rgba> color = parseColor(line, n, nbColors + 1);
+		if (!color) {
+			return;
+		}
+
+		++nbColors;
+		if (nbColors < maxNbColors) {
+			if (nbColors % options.nbColorsPerPal == 1) {
+				options.palSpec.emplace_back();
+			}
+			options.palSpec.back()[nbColors % options.nbColorsPerPal] = *color;
+		}
+	}
+
+	if (nbColors > maxNbColors) {
+		warning("GPL file contains %" PRIu16 " colors, but there can only be %" PRIu16
+		        "; ignoring extra",
+		        nbColors, maxNbColors);
+	}
+}
+
 static void parseACTFile(std::filebuf &file) {
 	// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1070626
 
@@ -432,8 +510,8 @@
 			break;
 		} else if (len != sizeof(buf)) {
 			error("GBC palette dump contains %zu 8-byte palette%s, plus %zu byte%s",
-			      options.palSpec.size(), options.palSpec.size() == 1 ? "" : "s",
-			      len, len == 1 ? "" : "s");
+			      options.palSpec.size(), options.palSpec.size() == 1 ? "" : "s", len,
+			      len == 1 ? "" : "s");
 			break;
 		}
 
@@ -457,6 +535,7 @@
 
 	static std::array parsers{
 	    std::tuple{"PSP", &parsePSPFile, std::ios::in    },
+	    std::tuple{"GPL", &parseGPLFile, std::ios::in    },
 	    std::tuple{"ACT", &parseACTFile, std::ios::binary},
 	    std::tuple{"ACO", &parseACOFile, std::ios::binary},
 	    std::tuple{"GBC", &parseGBCFile, std::ios::binary},