diff --git a/Makefile b/Makefile index 209d8808..bc88d6ab 100644 --- a/Makefile +++ b/Makefile @@ -108,6 +108,7 @@ rgbgfx_obj := \ src/gfx/main.o \ src/gfx/pal_packing.o \ src/gfx/pal_sorting.o \ + src/gfx/pal_spec.o \ src/gfx/process.o \ src/gfx/proto_palette.o \ src/gfx/reverse.o \ diff --git a/include/gfx/pal_spec.hpp b/include/gfx/pal_spec.hpp new file mode 100644 index 00000000..88f8446d --- /dev/null +++ b/include/gfx/pal_spec.hpp @@ -0,0 +1,16 @@ +/* + * This file is part of RGBDS. + * + * Copyright (c) 2022, Eldred Habert and RGBDS contributors. + * + * SPDX-License-Identifier: MIT + */ + +#ifndef RGBDS_GFX_PAL_SPEC_HPP +#define RGBDS_GFX_PAL_SPEC_HPP + +#include + +void parseInlinePalSpec(char const * const arg); + +#endif /* RGBDS_GFX_PAL_SPEC_HPP */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 77709a72..cf7723f9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -73,6 +73,7 @@ set(rgbgfx_src "gfx/main.cpp" "gfx/pal_packing.cpp" "gfx/pal_sorting.cpp" + "gfx/pal_spec.cpp" "gfx/process.cpp" "gfx/proto_palette.cpp" "gfx/reverse.cpp" diff --git a/src/gfx/main.cpp b/src/gfx/main.cpp index 8a125047..7ecdd533 100644 --- a/src/gfx/main.cpp +++ b/src/gfx/main.cpp @@ -26,6 +26,7 @@ #include "platform.h" #include "version.h" +#include "gfx/pal_spec.hpp" #include "gfx/process.hpp" #include "gfx/reverse.hpp" @@ -223,9 +224,8 @@ static void skipWhitespace(char *&arg) { static void parsePaletteSpec(char const *arg) { if (arg[0] == '#') { - // List of #rrggbb/#rgb colors, comma-separated, palettes are separated by colons options.palSpecType = Options::EXPLICIT; - // TODO + parseInlinePalSpec(arg); } else if (strcasecmp(arg, "embedded") == 0) { // Use PLTE, error out if missing options.palSpecType = Options::EMBEDDED; @@ -668,6 +668,14 @@ int main(int argc, char *argv[]) { } return "???"; }()); + if (options.palSpecType == Options::EXPLICIT) { + fputs("\t[\n", stderr); + for (std::array const &pal : options.palSpec) { + fprintf(stderr, "\t\t#%06x, #%06x, #%06x, #%06x,\n", pal[0].toCSS() >> 8, + pal[1].toCSS() >> 8, pal[2].toCSS() >> 8, pal[3].toCSS() >> 8); + } + fputs("\t]\n", stderr); + } fprintf(stderr, "\tDedup unit: %" PRIu16 "x%" PRIu16 " tiles\n", options.unitSize[0], options.unitSize[1]); fprintf(stderr, diff --git a/src/gfx/pal_spec.cpp b/src/gfx/pal_spec.cpp new file mode 100644 index 00000000..6bf95488 --- /dev/null +++ b/src/gfx/pal_spec.cpp @@ -0,0 +1,151 @@ +/* + * This file is part of RGBDS. + * + * Copyright (c) 2022, Eldred Habert and RGBDS contributors. + * + * SPDX-License-Identifier: MIT + */ + +#include "gfx/pal_spec.hpp" + +#include +#include +#include + +#include "gfx/main.hpp" + +using namespace std::string_view_literals; + +constexpr uint8_t nibble(char c) { + if (c >= 'a') { + assert(c <= 'f'); + return c - 'a' + 10; + } else if (c >= 'A') { + assert(c <= 'F'); + return c - 'A' + 10; + } else { + assert(c >= '0' && c <= '9'); + return c - '0'; + } +} + +constexpr uint8_t toHex(char c1, char c2) { + return nibble(c1) * 16 + nibble(c2); +} + +constexpr uint8_t singleToHex(char c) { + return toHex(c, c); +} + +void parseInlinePalSpec(char const * const rawArg) { + // List of #rrggbb/#rgb colors, comma-separated, palettes are separated by colons + + std::string_view arg(rawArg); + using size_type = decltype(arg)::size_type; + + auto parseError = [&rawArg, &arg](size_type ofs, size_type len, char const *fmt, + auto &&...args) { + (void)arg; // With NDEBUG, `arg` is otherwise not used + assert(ofs <= arg.length()); + assert(len <= arg.length()); + + error(fmt, args...); + fprintf(stderr, + "In inline palette spec: %s\n" + " ", + rawArg); + for (auto i = ofs; i; --i) { + putc(' ', stderr); + } + for (auto i = len; i; --i) { + putc('^', stderr); + } + 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 + + size_type n = 0; // Index into the argument + // TODO: store max `nbColors` ever reached, and compare against palette size later + size_t nbColors = 0; // Number of colors in the current palette + for (;;) { + ++n; // Ignore the '#' (checked either by caller or previous loop iteration) + + Rgba &color = options.palSpec.back()[nbColors]; + auto pos = std::min(arg.find_first_not_of("0123456789ABCDEFabcdef"sv, n), arg.length()); + switch (pos - n) { + case 3: + color = Rgba(singleToHex(arg[n + 0]), singleToHex(arg[n + 1]), singleToHex(arg[n + 2]), + 0xFF); + break; + case 6: + color = Rgba(toHex(arg[n + 0], arg[n + 1]), toHex(arg[n + 2], arg[n + 3]), + toHex(arg[n + 4], arg[n + 5]), 0xFF); + break; + case 0: + parseError(n - 1, 1, "Missing color after '#'"); + return; + default: + parseError(n, pos - n, "Unknown color specification"); + return; + } + n = pos; + + // Skip whitespace, if any + skipWhitespace(n); + + // Skip comma/colon, or end + if (n == arg.length()) { + break; + } + switch (arg[n]) { + case ',': + ++n; // Skip it + + ++nbColors; + + // A trailing comma may be followed by a colon + skipWhitespace(n); + if (n == arg.length()) { + break; + } else if (arg[n] != ':') { + if (nbColors == 4) { + parseError(n, 1, "Each palette can only contain up to 4 colors"); + return; + } + break; + } + [[fallthrough]]; + + case ':': + ++n; + skipWhitespace(n); + + nbColors = 0; // Start a new palette + // Avoid creating a spurious empty palette + if (n != arg.length()) { + options.palSpec.emplace_back(); + } + break; + + default: + parseError(n, 1, "Unexpected character, expected ',', ':', or end of argument"); + return; + } + + // Check again to allow trailing a comma/colon + if (n == arg.length()) { + break; + } + if (arg[n] != '#') { + parseError(n, 1, "Unexpected character, expected '#'"); + return; + } + } +}