From 6ff4f786800d40c1cf82a37cc648df65b7b3932a Mon Sep 17 00:00:00 2001 From: Zachary Hall Date: Sat, 18 Nov 2023 12:55:26 -0800 Subject: [PATCH] Prefer significant_start = 16, improve test, add debug option --- .vscode/settings.json | 9 +++- bitmapx16.cpp | 106 +++++++++++++++++++++++++++++------------- bitmapx16.h | 26 +++++++---- main.cpp | 56 ++++++++++++++++++++-- meson.build | 2 +- palette.cpp | 12 +++-- palette.h | 8 ++-- test.sh | 35 +++++++++----- 8 files changed, 191 insertions(+), 63 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7325b68..ee1b42f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,13 @@ "cstdint": "cpp", "iosfwd": "cpp", "*.ipp": "cpp", - "ios": "cpp" + "ios": "cpp", + "chrono": "cpp", + "random": "cpp", + "limits": "cpp", + "semaphore": "cpp", + "valarray": "cpp", + "ratio": "cpp", + "algorithm": "cpp" } } \ No newline at end of file diff --git a/bitmapx16.cpp b/bitmapx16.cpp index f571f72..850fe04 100644 --- a/bitmapx16.cpp +++ b/bitmapx16.cpp @@ -1,10 +1,12 @@ #include "bitmapx16.h" #include #include +#include #include +using namespace std::filesystem; #define X16_IMG_START (512+32) BitmapX16DebugFlags BitmapX16::debug = DebugNone; -float closeness_to_color(PaletteEntry a, PaletteEntry b) { +float BitmapX16::closeness_to_color(PaletteEntry a, PaletteEntry b) { float closeness = ((float)((((float)a.r - (float)b.r) * (1 << 4)) + (((float)a.g - (float)b.g) * (1 << 8)) + ((float)a.b - (float)b.b))); if (closeness < 0.0f) { closeness = -closeness; @@ -18,7 +20,7 @@ void BitmapX16::set_bpp(uint8_t bpp) { set_significant((1 << bpp) - 1); } } -uint8_t BitmapX16::get_bpp() { +uint8_t BitmapX16::get_bpp() const { return bpp; } void BitmapX16::set_significant(uint8_t value) { @@ -28,19 +30,19 @@ void BitmapX16::set_significant(uint8_t value) { significant_count = value; quantize_colors = true; } -uint8_t BitmapX16::get_significant() { +uint8_t BitmapX16::get_significant() const { return significant_count; } -size_t BitmapX16::get_width() { +size_t BitmapX16::get_width() const { return w; } -size_t BitmapX16::get_height() { +size_t BitmapX16::get_height() const { return h; } void BitmapX16::enable_dithering(bool enabled) { dither = enabled; } -bool BitmapX16::dithering_enabled() { +bool BitmapX16::dithering_enabled() const { return dither; } void BitmapX16::resize(size_t w, size_t h) { @@ -58,22 +60,18 @@ void BitmapX16::apply() { th = 0; } if (bpp == 0) { - bpp = 8; - quantize_colors = true; + set_bpp(8); } - if (significant_count == 0) { - significant_count = (1 << bpp) - 1; - quantize_colors = true; + if (significant_count == 0 || significant_count >= (1 << bpp)) { + set_significant((1 << bpp) - 1); } - if (quantize_colors) { - image->quantizeColors(significant_count); - image->quantizeDither(dither); - if (dither) { - image->quantizeDitherMethod(Magick::FloydSteinbergDitherMethod); - } - image->quantize(); - generate_palette(); + image->quantizeColors(significant_count); + image->quantizeDither(dither); + if (dither) { + image->quantizeDitherMethod(Magick::FloydSteinbergDitherMethod); } + image->quantize(); + generate_palette(); } uint8_t BitmapX16::extra_to_real_palette(uint8_t idx) { return image_palette_count + idx; @@ -157,7 +155,15 @@ void BitmapX16::load_x16(const char *filename) { vector pixels; bufsize = 3; buf.resize(bufsize); + if (!exists(filename)) { + printf("File not found!\n"); + throw std::exception(); + } std::ifstream infile(filename, std::ifstream::binary); + if (infile.bad()) { + printf("Failed to open file!\n"); + throw std::exception(); + } infile.read((char*)buf.data() + bufpos, bufsize - bufpos); bufpos += bufsize - bufpos; uint8_t magic[3] = {0x42, 0x4D, 0x58}; @@ -176,7 +182,7 @@ void BitmapX16::load_x16(const char *filename) { throw std::exception(); } bpp = buf[4]; - uint8_t vera_color_depth = buf[5]; // Ignore for now. + /*uint8_t vera_color_depth = buf[5];*/ // Ignore for now. pixels_per_byte = (8 / bpp); w = buf[6] | (buf[7] << 8); h = buf[8] | (buf[9] << 8); @@ -192,7 +198,7 @@ void BitmapX16::load_x16(const char *filename) { bufpos += bufsize - bufpos; for (size_t i = 0; i < 256; i++) { - palette[i] = PaletteEntry(buf.data() + (32+(i*2))); + palette[(uint8_t)(i-significant_start)] = PaletteEntry(buf.data() + (32+(i*2))); } // Border is always an extra palette entry. extra_palette_entries.push_back(palette[border]); @@ -232,6 +238,13 @@ void BitmapX16::write_pc(const char *filename) { } image->write(filename); } +PaletteEntry BitmapX16::get_palette_entry(uint8_t idx, bool extra) const { + if (extra) { + return extra_palette_entries.at(idx); + } else { + return palette[idx]; + } +} void BitmapX16::load_pc(const char *filename) { image = new Image(filename); w = image->columns(); @@ -265,7 +278,6 @@ BitmapX16::BitmapX16() { } void BitmapX16::generate_palette() { uint16_t max = (uint16_t)image->colorMapSize(); - uint8_t color[3]; if (max > 256) max = 256; if (bpp == 0) { if (max <= 4) { @@ -276,25 +288,36 @@ void BitmapX16::generate_palette() { bpp = 8; } } + size_t min = 256 - (1 << bpp); + if (min >= 16) { + significant_start = 16; + } if (significant_count == 0) { significant_count = max; } bitmask = (1 << bpp) - 1; - for (uint16_t i = 0; i < max; i++) { - ColorRGB map_color = image->colorMap(i); + for (uint16_t i = 0; i < significant_start; i++) { + palette[i] = PaletteEntry(); + } + for (uint16_t i = significant_start; i < max+significant_start; i++) { + ColorRGB map_color = image->colorMap(i-significant_start); palette[i] = PaletteEntry(map_color); } image_palette_count = max; - for (uint16_t i = max; i < 256; i++) { - if ((uint16_t)extra_palette_entries.size() > i - max) { - palette[i] = extra_palette_entries[i - max]; + for (uint16_t i = max+significant_start; i < 256; i++) { + if ((uint16_t)extra_palette_entries.size() > i - (max+significant_start)) { + palette[i] = extra_palette_entries[i - (max+significant_start)]; } else { palette[i] = PaletteEntry(); } } - return; - for (uint16_t i = 0; i < max; i++) { - printf("pallete[%u] = #%02x%02x%02x\n", i, palette[i].r, palette[i].g, palette[i].b); + if (debug & DebugShowPalette) { + for (size_t i = 0; i < 256; i++) { + uint8_t significant_end = significant_start+significant_count; + bool significant = i >= significant_start && i < significant_end; + bool extra = i >= significant_end && i < significant_end+extra_palette_entries.size(); + printf("palette[%02x] = %s %s\n", (uint16_t)i, palette[i].to_string().c_str(), significant ? "(Significant)" : extra ? "(Extra)" : ""); + } } } uint8_t BitmapX16::add_palette_entry(PaletteEntry entry) { @@ -305,7 +328,10 @@ uint8_t BitmapX16::color_to_palette_entry(const ColorRGB &rgb) { PaletteEntry color(rgb); float closeness = 100000.0f; uint8_t output; - for (size_t i = 0; i < significant_count; i++) { + if (debug & DebugShowCloseness) { + printf("Closest color for %s: ", color.to_string().c_str()); + } + for (size_t i = significant_start; i < significant_count+significant_start; i++) { float possibility_closeness = closeness_to_color(palette[i], color); //printf("Closeness: %f", possibility_closeness); if (possibility_closeness < closeness) { @@ -313,16 +339,32 @@ uint8_t BitmapX16::color_to_palette_entry(const ColorRGB &rgb) { closeness = possibility_closeness; } } + if (debug & DebugShowCloseness) { + PaletteEntry output_entry = palette[output]; + printf("%s\n", output_entry.to_string().c_str()); + } //PaletteEntry entry = palette[output]; //printf("Color: #%0x%0x%0x -> Palette entry#%0x%0x%0x, closeness: %f\n", color.r, color.g, color.b , entry.r, entry.g, entry.b, closeness); return output; } +void BitmapX16::set_debug_flag(BitmapX16DebugFlags flag, bool enabled) { + int value = (int)debug; + if (enabled) { + value |= (int)flag; + } else { + value &= ~(int)flag; + } + debug = (BitmapX16DebugFlags)value; +} BitmapX16::~BitmapX16() { if (image != nullptr) { delete image; } } -uint8_t BitmapX16::get_border_color() { +uint8_t BitmapX16::get_significant_start() const { + return significant_start; +} +uint8_t BitmapX16::get_border_color() const { return border; } void BitmapX16::set_border_color(uint8_t idx) { diff --git a/bitmapx16.h b/bitmapx16.h index 14a5de1..70b3a43 100644 --- a/bitmapx16.h +++ b/bitmapx16.h @@ -4,9 +4,10 @@ #include "palette.h" using namespace Magick; using std::vector; -enum BitmapX16DebugFlags { +enum BitmapX16DebugFlags : int { DebugNone = 0, DebugShowPalette = (1 << 0), + DebugShowCloseness = (1 << 1) }; class BitmapX16 { /// \brief Bits per pixel of the image @@ -75,45 +76,54 @@ class BitmapX16 { /// \returns The palette entry index uint8_t color_to_palette_entry(const ColorRGB &rgb); uint8_t extra_to_real_palette(uint8_t idx); + float closeness_to_color(PaletteEntry a, PaletteEntry b); public: /// \brief Sets the border color extra palette entry. /// \param idx The index of the palette entry, must be an index into the extra palette entry list. void set_border_color(uint8_t idx); /// \brief Gets the current extra palette entry of the border color /// \returns The border color palette entry from the extra palette entry list - uint8_t get_border_color(); + uint8_t get_border_color() const; /// \brief Adds an entry to the list of extra palette entries. Not guaranteed to continue existing after a new palette entry has been added. /// \param entry The new entry to add /// \returns The palette index within the extra palette entries of the new entry uint8_t add_palette_entry(PaletteEntry entry); + /// \brief Obtains a palette entry within the image + /// \param id The index of the palette entry + /// \param extra Set to true for an extra palette entry, false for a normal palette entry. + /// \returns The palette entry requested. + PaletteEntry get_palette_entry(uint8_t id, bool extra) const; /// \brief Sets the bits per pixel of the image /// \param bpp The bits per pixel of the image, one of 1, 2, 4, or 8 void set_bpp(uint8_t bpp); /// \brief Returns the bits per pixel of the image /// \returns The bits per pixel of the image, one of 1, 2, 4, or 8 - uint8_t get_bpp(); + uint8_t get_bpp() const; /// \brief Sets the maximum amount of colors to be used. /// \param value The maximum amount of colors to use. void set_significant(uint8_t value); /// \brief Returns the maximum amount of colors to be used /// \returns The maximum amount of colors once written - uint8_t get_significant(); + uint8_t get_significant() const; + /// \brief Gets the beginning index of the significant palette entries. + /// \returns The start index of significant palette entries. + uint8_t get_significant_start() const; /// \brief Queues a resize operation. Call BitmapX16::apply() to apply /// \param w The width to resize to /// \param h The height to resize to void queue_resize(size_t w, size_t h); /// \brief Gets the width of the image /// \returns The width of the image - size_t get_width(); + size_t get_width() const; /// \brief Gets the height of the image /// \returns The height of the image - size_t get_height(); + size_t get_height() const; /// \brief Enables or disables dithering /// \param enabled Pass true to enable, false to disable void enable_dithering(bool enabled); /// \brief Returns the status of the dithering flag /// \returns The value of the dithering flag - bool dithering_enabled(); + bool dithering_enabled() const; /// \brief Applies queued operations to the internal representation of the image void apply(); /// \brief Applies queued operations and writes the image to a PC-compatible file @@ -128,7 +138,7 @@ class BitmapX16 { /// \brief Loads a Commander X16-compatible image file /// \param filename The path to the file to load void load_x16(const char *filename); - + static void set_debug_flag(BitmapX16DebugFlags flag, bool enabled = true); /// \brief The debug flags to use. static BitmapX16DebugFlags debug; /// \brief Constructs an unloaded X16-compatible bitmap image. Call load_pc or load_x16 before using any other functions. diff --git a/main.cpp b/main.cpp index 22b97aa..93d8c16 100644 --- a/main.cpp +++ b/main.cpp @@ -31,6 +31,10 @@ void usage() { printf("\tIf possible, adds a border color with the specified RGB values which are in the range of 0-15.\n"); printf("-reverse\n"); printf("\tConverts to PC formats. Incompatible with -dither, -type, and -significant - they will be ignored.\n"); + printf("-debug \n"); + printf("\tEnables debugging flags. May contain any number of single-letter debugging flags.\n"); + printf("\tFlags: p = show palette entry, c = show palette closeness lookups\n"); + printf("\tUse an ! before a flag to disable it.\n"); printf("-help\n"); printf("\tDisplays this help message.\n"); exit(1); @@ -40,6 +44,7 @@ int main(int argc, char **argv) { size_t tw = 0, th = 0; // Target width & height; uint8_t tbpp = 0; uint16_t tcolorcount = 0; + const char *error_msg_part = "load the image"; bool dither = false; bool reverse = false; uint8_t br, bg, bb; @@ -177,6 +182,35 @@ int main(int argc, char **argv) { argv++; } else if (!strcmp(argv[0], "-help")) { usage(); + } else if (!strcmp(argv[0], "-debug")) { + argc--; + argv++; + if (!argc || argv[0][0] == '-') { + usage(); + } + bool flag_enable = true; + for (size_t i = 0; i < strlen(argv[0]); i++) { + switch (argv[0][i]) { + case '!': { + flag_enable = false; + } break; + case 'p': { + BitmapX16::set_debug_flag(DebugShowPalette, flag_enable); + } break; + case 'c': { + BitmapX16::set_debug_flag(DebugShowCloseness, flag_enable); + } break; + default: { + printf("Error: Invalid debugging flag.\n"); + usage(); + } break; + } + if (argv[0][i] != '!') { + flag_enable = true; + } + } + argc--; + argv++; } else { printf("Error: Invalid command line argument.\n"); usage(); @@ -191,19 +225,27 @@ int main(int argc, char **argv) { try { BitmapX16 bitmap; if (reverse) { - printf("Converting %s to a PC format...\n", input); + printf("Loading X16 bitmap file '%s' to be convertedto a PC format...\n", input); bitmap.load_x16(input); + printf("Image has %u significant colors starting at %u and is %u bpp\n", bitmap.get_significant(), bitmap.get_significant_start(), bitmap.get_bpp()); + PaletteEntry border = bitmap.get_palette_entry(bitmap.get_border_color(), true); + printf("Border color: %s\n", border.to_string().c_str()); } else { - printf("Using at most %u colors at %u bpp\n", tcolorcount, tbpp); - printf("Converting %s to BMX16...\n", input); + printf("Loading PC image file '%s' to be converted to the X16 bitmap format...\n", input); + printf("Using at most %u colors (excluding border color) at %u bpp\n", tcolorcount, tbpp); bitmap.load_pc(input); } if (tw != 0 && th != 0) { + error_msg_part = "resize the image"; bitmap.queue_resize(tw, th); } if (reverse) { + error_msg_part = "write the file"; + printf("Writing PC image file...\n"); bitmap.write_pc(output); } else { + error_msg_part = "apply the settings"; + printf("Applying settings...\n"); bitmap.enable_dithering(dither); bitmap.set_bpp(tbpp); bitmap.set_significant(tcolorcount); @@ -212,10 +254,16 @@ int main(int argc, char **argv) { bitmap.set_border_color(bitmap.add_palette_entry(entry)); } bitmap.apply(); + uint8_t significant_start = bitmap.get_significant_start(); + uint8_t significant_count = bitmap.get_significant(); + uint8_t significant_end = significant_start + significant_count; + printf("Significant colors start at %u and end at %u (%u entries)\n", significant_start, significant_end, significant_count); + printf("Writing X16 bitmap file...\n"); + error_msg_part = "write the file"; bitmap.write_x16(output); } } catch (std::exception &e) { - printf("Failed to convert image '%s'!\n", input); + printf("Failed to %s!\n", error_msg_part); } return 0; } diff --git a/meson.build b/meson.build index 9c20575..f09f4c5 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project('graphicsconverter', 'cpp', version : '0.1', default_options : ['warning_level=3', - 'cpp_std=c++14', + 'cpp_std=c++17', 'default_library=static']) deps = [ diff --git a/palette.cpp b/palette.cpp index bc8533c..d86b5ec 100644 --- a/palette.cpp +++ b/palette.cpp @@ -1,15 +1,21 @@ #include "palette.h" -ColorRGB PaletteEntry::toColor() { +ColorRGB PaletteEntry::toColor() const { ColorRGB color; color.red(((double)r) / 15.0); color.green(((double)g) / 15.0); color.blue(((double)b) / 15.0); return color; } -void PaletteEntry::write(uint8_t *ptr) { +void PaletteEntry::write(uint8_t *ptr) const { ptr[0] = ((g & 0b1111) << 4) | (b & 0b1111); ptr[1] = r; } +std::string PaletteEntry::to_string() const { + char buf[65]; + snprintf(buf, 64, "#%0x%0x%0x", r, g, b); + std::string output(buf); + return output; +} PaletteEntry::PaletteEntry(uint8_t *ptr) { b = (ptr[0] & 0b1111); g = ((ptr[0] >> 4) & 0b1111); @@ -25,7 +31,7 @@ PaletteEntry::PaletteEntry(const ColorRGB &rgb) { g = (uint8_t)((rgb.green()) * 15); b = (uint8_t)((rgb.blue()) * 15); } -uint16_t PaletteEntry::hash() { +uint16_t PaletteEntry::hash() const { return ((r & 0b1111) << 8) | ((g & 0b1111) << 4) | (b & 0b1111); } PaletteEntry::PaletteEntry() { diff --git a/palette.h b/palette.h index 2f6dbb2..38a1637 100644 --- a/palette.h +++ b/palette.h @@ -1,15 +1,17 @@ #pragma once #include #include +#include using namespace Magick; class PaletteEntry { public: uint8_t r; uint8_t g; uint8_t b; - uint16_t hash(); - void write(uint8_t *ptr); - ColorRGB toColor(); + uint16_t hash() const; + void write(uint8_t *ptr) const; + ColorRGB toColor() const; + std::string to_string() const; PaletteEntry(); PaletteEntry(uint8_t *ptr); PaletteEntry(uint8_t r, uint8_t g, uint8_t b); diff --git a/test.sh b/test.sh index 664a6fd..2d1d4c0 100755 --- a/test.sh +++ b/test.sh @@ -8,12 +8,14 @@ $0 usage: Adds an image input at the specified path to the image list. -b|--output-bpp Adds a bitdepth to the list to test with --r|--resize +-r|--resize x Adds a resize to the list to test with --no-reverse Disables reverse operation testing --no-dither Disables dithering testing +-d|--debug + Use debug flags with the converter program. -n|--no-defaults Disables default settings EOF @@ -24,16 +26,18 @@ cd "$(dirname "$0")" converter="./builddir/b16converter" prebuilt=0 images=() -bpp=() +bpps=() resize=() enable_defaults=1 dither=1 reverse=1 enable_reverse=1 enable_dither=1 +debug_flags="" outdir="testout" -OPTIONS=$(getopt -o "hp:i:r::no:" --long "help,use-program:,input-image:,output-bpp:,resize::,no-defaults,output-dir:,no-reverse,no-dither" -- "$@") +OPTIONS=$(getopt -o "b:hp:i:r:no:d:" --long "help,use-program:,input-image:,output-bpp:,resize:,no-defaults,output-dir:,debug:,no-reverse,no-dither" -- "$@") if [ $? != 0 ]; then + echo "Getopt error." usage fi eval set -- "$OPTIONS" @@ -60,7 +64,7 @@ while [ -n "$1" ]; do shift 2 ;; -r|--resize) - resize+="$2x$3" + resize+="$2" shift 2 ;; -n|--no-defaults) @@ -75,18 +79,23 @@ while [ -n "$1" ]; do enable_dither=0 shift ;; + -d|--debug) + debug_flags="${debug_flags}$2" + shift 2 + ;; --) shift break ;; - *) + *) + echo "Invalid option: $0" usage ;; esac done if [ $enable_defaults -ne 0 ]; then images+=("TEST.png" "PACK.png") - bpp+=(1 2 4 8) + bpps+=(1 2 4 8) resize+=("8x8" "16x16" "32x32" "64x64" "320x240" "640x480") fi if [ $prebuilt -eq 0 ]; then @@ -94,21 +103,25 @@ if [ $prebuilt -eq 0 ]; then meson compile -C builddir || exit $? fi mkdir -p "$outdir" +run() { + printf "Running: %s\n" "$*" + "$@" +} for img in "${images[@]}"; do - for bpp in "${bpp[@]}"; do + for bpp in "${bpps[@]}"; do for size in "${resize[@]}"; do width="$(echo -n "$size" | cut -dx -f1)" height="$(echo -n "$size" | cut -dx -f2)" name="$(basename "$img" | sed 's/\.png$//')" name="$(printf "%s.%sP.%sB" "$name" "$width" "$bpp")" - ./builddir/b16converter -in "$img" -out "$outdir/$name.B16" -bpp "$bpp" -resize "$width" "$height" -border 15 0 15 + run "$converter" -in "$img" -out "$outdir/$name.B16" -bpp "$bpp" -resize "$width" "$height" -border 15 0 15 -debug "$debug_flags" if [ $enable_dither -ne 0 ]; then - ./builddir/b16converter -in "$img" -out "$outdir/$name.D.B16" -bpp "$bpp" -resize "$width" "$height" -dither -border 15 0 15 + run "$converter" -in "$img" -out "$outdir/$name.D.B16" -bpp "$bpp" -resize "$width" "$height" -dither -border 15 0 15 -debug "$debug_flags" fi if [ $enable_reverse -ne 0 ]; then - ./builddir/b16converter -reverse -in "$outdir/$name.B16" -out "$outdir/$name.PNG" -resize "$width" "$height" + run "$converter" -reverse -in "$outdir/$name.B16" -out "$outdir/$name.PNG" -resize "$width" "$height" -debug "$debug_flags" if [ $enable_dither -ne 0 ]; then - ./builddir/b16converter -reverse -in "$outdir/$name.D.B16" -out "$outdir/$name.D.PNG" -resize "$width" "$height" -dither + run "$converter" -reverse -in "$outdir/$name.D.B16" -out "$outdir/$name.D.PNG" -resize "$width" "$height" -dither -debug "$debug_flags" fi fi done