diff --git a/direct/src/p3d/DeploymentTools.py b/direct/src/p3d/DeploymentTools.py index 1849556dcd..ebc20bfda9 100644 --- a/direct/src/p3d/DeploymentTools.py +++ b/direct/src/p3d/DeploymentTools.py @@ -344,6 +344,7 @@ class Icon: if required_size * 2 in sizes: from_size = required_size * 2 else: + from_size = 0 for from_size in sizes: if from_size > required_size: break @@ -367,7 +368,7 @@ class Icon: # XOR mask if bpp == 24: # Align rows to 4-byte boundary - rowalign = '\0' * (-(size * 3) & 3) + rowalign = b'\0' * (-(size * 3) & 3) for y in xrange(size): for x in xrange(size): r, g, b = image.getXel(x, size - y - 1) @@ -383,35 +384,14 @@ class Icon: elif bpp == 8: # We'll have to generate a palette of 256 colors. hist = PNMImage.Histogram() - if image.hasAlpha(): - # Make a copy without alpha channel. - image2 = PNMImage(image) + image2 = PNMImage(image) + if image2.hasAlpha(): image2.premultiplyAlpha() image2.removeAlpha() - else: - image2 = image + image2.quantize(256) image2.make_histogram(hist) colors = list(hist.get_pixels()) - if len(colors) > 256: - # Palette too large; remove infrequent colors. - colors.sort(key=hist.get_count, reverse=True) - - # Find the closest color on the palette matching each color - # that didn't fit. This is certainly not the best palette - # generation code, but it'll do for now. - closest_indices = [] - for color in colors[256:]: - closest_index = 0 - closest_diff = 1025 - for i, closest_color in enumerate(colors[:256]): - diff = abs(color.get_red() - closest_color.get_red()) \ - + abs(color.get_green() - closest_color.get_green()) \ - + abs(color.get_blue() - closest_color.get_blue()) - if diff < closest_diff: - closest_index = i - closest_diff = diff - assert closest_diff < 100 - closest_indices.append(closest_index) + assert len(colors) <= 256 # Write the palette. i = 0 @@ -503,7 +483,7 @@ class Icon: if size > 256: continue elif size == 256: - ico.write('\0\0') + ico.write(b'\0\0') else: ico.write(struct.pack('= 0 && x < _x_size && y >= 0 && y < _y_size, LRGBColorf::zero()); - - const xel &col = row(y)[x]; - - switch (_xel_encoding) { - case XE_generic: - case XE_generic_alpha: - return LRGBColorf(col.r, col.g, col.b) * _inv_maxval; - - case XE_generic_sRGB: - case XE_generic_sRGB_alpha: - return LRGBColorf( - decode_sRGB_float(col.r * _inv_maxval), - decode_sRGB_float(col.g * _inv_maxval), - decode_sRGB_float(col.b * _inv_maxval)); - - case XE_uchar_sRGB: - case XE_uchar_sRGB_alpha: - case XE_uchar_sRGB_sse2: - case XE_uchar_sRGB_alpha_sse2: - return LRGBColorf( - decode_sRGB_float((unsigned char)col.r), - decode_sRGB_float((unsigned char)col.g), - decode_sRGB_float((unsigned char)col.b)); - - case XE_scRGB: - case XE_scRGB_alpha: - return LRGBColorf((int)col.r - 4096, - (int)col.g - 4096, - (int)col.b - 4096) * (1.f / 8192.f); - - default: - return LRGBColorf(0); - } + return from_val(row(y)[x]); } /** @@ -521,50 +578,7 @@ get_xel(int x, int y) const { INLINE void PNMImage:: set_xel(int x, int y, const LRGBColorf &value) { nassertv(x >= 0 && x < _x_size && y >= 0 && y < _y_size); - - xel &col = row(y)[x]; - - switch (_xel_encoding) { - case XE_generic: - case XE_generic_alpha: - { - LRGBColorf scaled = value * get_maxval() + 0.5f; - col.r = clamp_val((int)scaled[0]); - col.g = clamp_val((int)scaled[1]); - col.b = clamp_val((int)scaled[2]); - } - break; - - case XE_generic_sRGB: - case XE_generic_sRGB_alpha: - col.r = clamp_val((int) - (encode_sRGB_float(value[0]) * get_maxval() + 0.5f)); - col.g = clamp_val((int) - (encode_sRGB_float(value[1]) * get_maxval() + 0.5f)); - col.b = clamp_val((int) - (encode_sRGB_float(value[2]) * get_maxval() + 0.5f)); - break; - - case XE_uchar_sRGB: - case XE_uchar_sRGB_alpha: - encode_sRGB_uchar(LColorf(value, 0.0f), col); - break; - - case XE_uchar_sRGB_sse2: - case XE_uchar_sRGB_alpha_sse2: - encode_sRGB_uchar_sse2(LColorf(value, 0.0f), col); - break; - - case XE_scRGB: - case XE_scRGB_alpha: - { - LRGBColorf scaled = value * 8192.f + 4096.5f; - col.r = std::min(std::max(0, (int)scaled[0]), 65535); - col.g = std::min(std::max(0, (int)scaled[1]), 65535); - col.b = std::min(std::max(0, (int)scaled[2]), 65535); - } - break; - } + row(y)[x] = to_val(value); } /** diff --git a/panda/src/pnmimage/pnmImage.cxx b/panda/src/pnmimage/pnmImage.cxx index a26528f99a..a8deae0b6b 100644 --- a/panda/src/pnmimage/pnmImage.cxx +++ b/panda/src/pnmimage/pnmImage.cxx @@ -1928,6 +1928,51 @@ make_histogram(PNMImage::Histogram &histogram) { histogram.swap(pixels, hist_map); } +/** + * Reduces the number of unique colors in the image to (at most) the given + * count. Fewer colors than requested may be left in the image after this + * operation, but never more. + * + * At present, this is only supported on images without an alpha channel. + * + * @since 1.10.5 + */ +void PNMImage:: +quantize(size_t max_colors) { + nassertv(_array != nullptr); + nassertv(!has_alpha()); + size_t array_size = _x_size * _y_size; + + // Get all the unique colors in this image. + pmap color_map; + for (size_t i = 0; i < array_size; ++i) { + color_map[_array[i]]; + } + + size_t num_colors = color_map.size(); + if (num_colors <= max_colors) { + // We are already down to the requested number of colors. + return; + } + + // Collect all the colors into a contiguous array. + xel *colors = (xel *)alloca(num_colors * sizeof(xel)); + size_t i = 0; + for (pmap::const_iterator it = color_map.begin(); + it != color_map.end(); ++it) { + colors[i++] = it->first; + } + nassertv(i == num_colors); + + // Apply the median cut algorithm, which will give us a color map. + r_quantize(color_map, max_colors, colors, num_colors); + + // Replace all the existing colors with the corresponding bucket average. + for (size_t i = 0; i < array_size; ++i) { + _array[i] = color_map[_array[i]]; + } +} + /** * Fills the image with a grayscale perlin noise pattern based on the * indicated parameters. Uses set_xel to set the grayscale values. The sx @@ -2161,6 +2206,92 @@ setup_encoding() { } } +/** + * Recursive implementation of quantize() using the median cut algorithm. + */ +void PNMImage:: +r_quantize(pmap &color_map, size_t max_colors, + xel *colors, size_t num_colors) { + if (num_colors <= max_colors) { + // All points in this bucket can be preserved 1:1. + for (size_t i = 0; i < num_colors; ++i) { + const xel &col = colors[i]; + color_map[col] = col; + } + return; + } + else if (max_colors == 1) { + // We've reached the target. Calculate the average, in linear space. + LRGBColorf avg(0); + for (size_t i = 0; i < num_colors; ++i) { + avg += from_val(colors[i]); + } + avg *= 1.0f / num_colors; + xel avg_val = to_val(avg); + + // Map all colors in this bucket to the avg. + for (size_t i = 0; i < num_colors; ++i) { + color_map[colors[i]] = avg_val; + } + return; + } + else if (max_colors == 0) { + // Not sure how this happens, but we can't preserve any color here. + return; + } + + // Find the minimum/maximum RGB values. We should probably do this in + // linear space, but eh. + xelval min_r = _maxval; + xelval min_g = _maxval; + xelval min_b = _maxval; + xelval max_r = 0, max_g = 0, max_b = 0; + for (size_t i = 0; i < num_colors; ++i) { + const xel &col = colors[i]; + min_r = std::min(min_r, col.r); + max_r = std::max(max_r, col.r); + min_g = std::min(min_g, col.g); + max_g = std::max(max_g, col.g); + min_b = std::min(min_b, col.b); + max_b = std::max(max_b, col.b); + } + + int diff_r = max_r - min_r; + int diff_g = max_g - min_g; + int diff_b = max_b - min_b; + + auto sort_by_red = [](const xel &c1, const xel &c2) { + return c1.r < c2.r; + }; + auto sort_by_green = [](const xel &c1, const xel &c2) { + return c1.g < c2.g; + }; + auto sort_by_blue = [](const xel &c1, const xel &c2) { + return c1.b < c2.b; + }; + + // Sort by the component with the most variation. + if (diff_g >= diff_r) { + if (diff_g >= diff_b) { + std::sort(colors, colors + num_colors, sort_by_green); + } else { + std::sort(colors, colors + num_colors, sort_by_blue); + } + } else if (diff_r >= diff_b) { + std::sort(colors, colors + num_colors, sort_by_red); + } else { + std::sort(colors, colors + num_colors, sort_by_blue); + } + + // Subdivide the sorted colors into two buckets, and recurse. + size_t max_colors_1 = max_colors / 2; + size_t max_colors_2 = max_colors - max_colors_1; + size_t num_colors_1 = num_colors / 2; + size_t num_colors_2 = num_colors - num_colors_1; + r_quantize(color_map, max_colors_1, colors, num_colors_1); + r_quantize(color_map, max_colors_2, colors + num_colors_1, num_colors_2); +} + /** * Recursively fills in the minimum distance measured from a certain set of * points into the gray channel. diff --git a/panda/src/pnmimage/pnmImage.h b/panda/src/pnmimage/pnmImage.h index 494acb8029..da1e2d0553 100644 --- a/panda/src/pnmimage/pnmImage.h +++ b/panda/src/pnmimage/pnmImage.h @@ -68,8 +68,10 @@ PUBLISHED: INLINE ~PNMImage(); INLINE xelval clamp_val(int input_value) const; + INLINE xel to_val(const LRGBColorf &input_value) const; INLINE xelval to_val(float input_value) const; INLINE xelval to_alpha_val(float input_value) const; + INLINE LRGBColorf from_val(const xel &input_value) const; INLINE float from_val(xelval input_value) const; INLINE float from_alpha_val(xelval input_value) const; @@ -254,6 +256,7 @@ PUBLISHED: int xborder = 0, int yborder = 0); void make_histogram(Histogram &hist); + void quantize(size_t max_colors); BLOCKING void perlin_noise_fill(float sx, float sy, int table_size = 256, unsigned long seed = 0); void perlin_noise_fill(StackedPerlinNoise2 &perlin); @@ -346,6 +349,9 @@ private: void setup_rc(); void setup_encoding(); + void r_quantize(pmap &color_map, size_t max_colors, + xel *colors, size_t num_colors); + PUBLISHED: PNMImage operator ~() const; diff --git a/panda/src/pnmimage/pnmimage_base.h b/panda/src/pnmimage/pnmimage_base.h index ca1f356c48..aa5474e82a 100644 --- a/panda/src/pnmimage/pnmimage_base.h +++ b/panda/src/pnmimage/pnmimage_base.h @@ -59,6 +59,22 @@ PUBLISHED: void operator *= (const double mult) { r *= mult; g *= mult; b *= mult; } + bool operator == (const pixel &other) { + return r == other.r && g == other.g && r == other.r; + } + bool operator != (const pixel &other) { + return r != other.r || g != other.g || r != other.r; + } + bool operator < (const pixel &other) const { + if (r != other.r) { + return r < other.r; + } + if (g != other.g) { + return g < other.g; + } + return b < other.b; + } + #ifdef HAVE_PYTHON static int size() { return 3; } void output(std::ostream &out) { diff --git a/pandatool/src/assimp/config_assimp.h b/pandatool/src/assimp/config_assimp.h index 898497eeb2..e17e2caae6 100644 --- a/pandatool/src/assimp/config_assimp.h +++ b/pandatool/src/assimp/config_assimp.h @@ -15,6 +15,7 @@ #define CONFIG_ASSIMP_H #include "pandatoolbase.h" +#include "notifyCategoryProxy.h" #include "configVariableBool.h" #include "configVariableDouble.h" #include "dconfig.h" diff --git a/pandatool/src/miscprogs/binToC.cxx b/pandatool/src/miscprogs/binToC.cxx index 6eff47c9d1..ae9fbc4090 100644 --- a/pandatool/src/miscprogs/binToC.cxx +++ b/pandatool/src/miscprogs/binToC.cxx @@ -99,8 +99,7 @@ run() { out << std::hex << std::setfill('0'); int count = 0; int col = 0; - unsigned int ch; - ch = in.get(); + int ch = in.get(); while (!in.fail() && ch != EOF) { if (col == 0) { out << "\n "; @@ -110,7 +109,7 @@ run() { } else { out << ", "; } - out << "0x" << std::setw(2) << ch; + out << "0x" << std::setw(2) << (unsigned int)ch; col++; count++; ch = in.get(); diff --git a/pandatool/src/ptloader/config_ptloader.h b/pandatool/src/ptloader/config_ptloader.h index b195083b2b..fc324dc939 100644 --- a/pandatool/src/ptloader/config_ptloader.h +++ b/pandatool/src/ptloader/config_ptloader.h @@ -15,7 +15,7 @@ #define CONFIG_PTLOADER_H #include "pandatoolbase.h" - +#include "notifyCategoryProxy.h" #include "dconfig.h" #include "distanceUnit.h" #include "configVariableEnum.h" diff --git a/tests/pnmimage/test_pnmimage.py b/tests/pnmimage/test_pnmimage.py index 0826e4f0b8..40d8a88683 100644 --- a/tests/pnmimage/test_pnmimage.py +++ b/tests/pnmimage/test_pnmimage.py @@ -1,4 +1,5 @@ from panda3d.core import PNMImage, PNMImageHeader +from random import randint def test_pixelspec_ctor(): @@ -19,3 +20,53 @@ def test_pixelspec_coerce(): img = PNMImage(1, 1, 4) img.set_pixel(0, 0, (1, 2, 3, 4)) assert img.get_pixel(0, 0) == (1, 2, 3, 4) + + +def test_pnmimage_to_val(): + img = PNMImage(1, 1) + assert img.to_val(-0.5) == 0 + assert img.to_val(0.0) == 0 + assert img.to_val(0.5) == 128 + assert img.to_val(1.0) == 255 + assert img.to_val(2.0) == 255 + + +def test_pnmimage_from_val(): + img = PNMImage(1, 1) + assert img.from_val(0) == 0.0 + assert img.to_val(img.from_val(128)) == 128 + assert img.from_val(255) == 1.0 + + +def test_pnmimage_quantize(): + img = PNMImage(32, 32, 3) + + for x in range(32): + for y in range(32): + img.set_xel_val(x, y, randint(0, 100), randint(50, 100), randint(0, 1)) + + hist = PNMImage.Histogram() + img.make_histogram(hist) + num_colors = hist.get_num_pixels() + assert num_colors > 100 + + img2 = PNMImage(img) + img2.quantize(100) + hist = PNMImage.Histogram() + img2.make_histogram(hist) + assert hist.get_num_pixels() <= 100 + + # Make sure that this is reasonably close + max_dist = 0 + for x in range(32): + for y in range(32): + diff = img.get_xel(x, y) - img2.get_xel(x, y) + max_dist = max(max_dist, diff.length_squared()) + + # Also make sure that they are not out of range of the original + col = img2.get_xel_val(x, y) + assert col.r <= 100 + assert col.g >= 50 and col.g <= 100 + assert col.b in (0, 1) + + assert max_dist < 0.1 ** 2 diff --git a/tests/putil/test_modifierbuttons.py b/tests/putil/test_modifierbuttons.py new file mode 100644 index 0000000000..289966d1df --- /dev/null +++ b/tests/putil/test_modifierbuttons.py @@ -0,0 +1,35 @@ +from panda3d.core import ModifierButtons + + +def test_modifierbuttons_empty(): + # Tests the initial state of a ModifierButtons object. + btns = ModifierButtons() + assert btns == ModifierButtons(btns) + assert btns != ModifierButtons() + assert btns.matches(ModifierButtons()) + assert not btns.is_down("alt") + assert not btns.is_any_down() + assert not btns.has_button("alt") + assert btns.get_prefix() == "" + assert btns.get_num_buttons() == 0 + assert len(btns.buttons) == 0 + + +def test_modifierbuttons_cow(): + # Tests the copy-on-write mechanism of the button list. + btns1 = ModifierButtons() + btns1.add_button("space") + + # Modifying original should not affect copy + btns2 = ModifierButtons(btns1) + assert tuple(btns2.buttons) == tuple(btns1.buttons) + btns1.add_button("enter") + assert tuple(btns1.buttons) == ("space", "enter") + assert tuple(btns2.buttons) == ("space",) + + # Modifying copy should not affect original + btns3 = ModifierButtons(btns2) + assert tuple(btns3.buttons) == tuple(btns2.buttons) + btns3.add_button("escape") + assert tuple(btns2.buttons) == ("space",) + assert tuple(btns3.buttons) == ("space", "escape")