From 28b541d49a0e05685254626d698f168acf979cc8 Mon Sep 17 00:00:00 2001 From: drewcassidy Date: Tue, 30 Mar 2021 02:36:27 -0700 Subject: [PATCH] Improved subscripting --- quicktex/Texture.h | 9 +++++---- quicktex/_bindings.cpp | 16 ++++++---------- quicktex/_bindings.h | 41 +++++++++++++++++++++++++++++++++++------ tests/test_texture.py | 30 ++++++++++++++++++------------ 4 files changed, 64 insertions(+), 32 deletions(-) diff --git a/quicktex/Texture.h b/quicktex/Texture.h index 74a220e..83d55ce 100644 --- a/quicktex/Texture.h +++ b/quicktex/Texture.h @@ -203,16 +203,17 @@ template class BlockTexture : public Texture { constexpr int BlocksX() const { return _width / B::Width; } constexpr int BlocksY() const { return _height / B::Height; } + constexpr std::tuple BlocksXY() const { return std::tuple(BlocksX(), BlocksY()); } virtual B GetBlock(int x, int y) const { - if (x < 0 || x >= BlocksX()) throw std::invalid_argument("x value out of range."); - if (y < 0 || y >= BlocksY()) throw std::invalid_argument("y value out of range."); + if (x < 0 || x >= BlocksX()) throw std::range_error("x value out of range."); + if (y < 0 || y >= BlocksY()) throw std::range_error("y value out of range."); return _blocks[x + (y * _width)]; } virtual void SetBlock(int x, int y, const B &val) { - if (x < 0 || x >= BlocksX()) throw std::invalid_argument("x value out of range."); - if (y < 0 || y >= BlocksY()) throw std::invalid_argument("y value out of range."); + if (x < 0 || x >= BlocksX()) throw std::range_error("x value out of range."); + if (y < 0 || y >= BlocksY()) throw std::range_error("y value out of range."); _blocks[x + (y * _width)] = val; } diff --git a/quicktex/_bindings.cpp b/quicktex/_bindings.cpp index efb6b9a..9963bad 100644 --- a/quicktex/_bindings.cpp +++ b/quicktex/_bindings.cpp @@ -88,6 +88,8 @@ PYBIND11_MODULE(_quicktex, m) { py::options options; + // Texture + py::class_ texture(m, "Texture", py::buffer_protocol()); texture.def_property_readonly("size", &Texture::Size); @@ -98,19 +100,13 @@ PYBIND11_MODULE(_quicktex, m) { texture.def_buffer([](Texture &t) { return py::buffer_info(t.Data(), t.Size()); }); texture.def("tobytes", [](const Texture &t) { return py::bytes(reinterpret_cast(t.Data()), t.Size()); }); + //RawTexture + py::class_ raw_texture(m, "RawTexture", texture); raw_texture.def(py::init(), "width"_a, "height"_a); - raw_texture.def("__getitem__", [](RawTexture &t, std::tuple pnt) { - int x, y; - PyIndex2D(pnt, t.Dimensions(), x, y); - return t.GetPixel(x, y); - }); - raw_texture.def("__setitem__", [](RawTexture &t, std::tuple pnt, Color val) { - int x, y; - PyIndex2D(pnt, t.Dimensions(), x, y); - t.SetPixel(x, y, val); - }); + + DefSubscript2D(raw_texture, &RawTexture::GetPixel, &RawTexture::SetPixel, &RawTexture::Dimensions); raw_texture.def_static("frombytes", &BufferToTexture, "data"_a, "width"_a, "height"_a); diff --git a/quicktex/_bindings.h b/quicktex/_bindings.h index b205e65..0e524d0 100644 --- a/quicktex/_bindings.h +++ b/quicktex/_bindings.h @@ -25,6 +25,7 @@ #include #include #include +#include #include "Block.h" #include "Color.h" @@ -60,16 +61,41 @@ template T BufferToTexture(py::buffer buf, int width, int height) { return output; } -inline int PyIndex(int val, int size) { - if (val < -size) throw std::range_error("index out of range"); - if (val >= size) throw std::range_error("index out of range"); +inline int PyIndex(int val, int size, std::string name = "index") { + if (val < -size || val >= size) throw std::out_of_range(name + " value out of range"); if (val < 0) return size + val; return val; } -inline void PyIndex2D(std::tuple pnt, std::tuple size, int& x, int& y) { - x = PyIndex(std::get<0>(pnt), std::get<0>(size)); - y = PyIndex(std::get<1>(pnt), std::get<1>(size)); +template void DefSubscript(py::class_ t, Getter&& get, Setter&& set, Extent&& ext) { + using V = typename std::invoke_result::type; + t.def( + "__getitem__", [get, ext](T& self, int index) { return (self.*get)(PyIndex(index, (self.*ext)())); }, "key"_a); + t.def( + "__setitem__", [set, ext](T& self, int index, V val) { (self.*set)(PyIndex(index, (self.*ext)()), val); }, "key"_a, "value"_a); +} + +template void DefSubscript2D(py::class_ t, Getter&& get, Setter&& set, Extent&& ext) { + using V = typename std::invoke_result::type; + using Coords = std::tuple; + t.def( + "__getitem__", + [get, ext](T& self, Coords pnt) { + Coords s = (self.*ext)(); + int x = PyIndex(std::get<0>(pnt), std::get<0>(s), "x"); + int y = PyIndex(std::get<1>(pnt), std::get<1>(s), "y"); + return (self.*get)(x, y); + }, + "key"_a); + t.def( + "__setitem__", + [set, ext](T& self, Coords pnt, V val) { + Coords s = (self.*ext)(); + int x = PyIndex(std::get<0>(pnt), std::get<0>(s), "x"); + int y = PyIndex(std::get<1>(pnt), std::get<1>(s), "y"); + (self.*set)(x, y, val); + }, + "key"_a, "value"_a); } template py::class_> BindBlockTexture(py::module_& m, const char* name) { @@ -87,5 +113,8 @@ template py::class_> BindBlockTexture(py::module_& block_texture.def_property_readonly("blocks_x", &BTex::BlocksX); block_texture.def_property_readonly("blocks_y", &BTex::BlocksY); + block_texture.def_property_readonly("blocks_xy", &BTex::BlocksXY); + + DefSubscript2D(block_texture, &BTex::GetPixel, &BTex::SetPixel, &BTex::BlocksXY); } } // namespace quicktex::bindings \ No newline at end of file diff --git a/tests/test_texture.py b/tests/test_texture.py index 0c96576..25481d8 100644 --- a/tests/test_texture.py +++ b/tests/test_texture.py @@ -7,21 +7,23 @@ from PIL import Image class TestRawTexture(unittest.TestCase): + boilerplate = Image.open(images.image_path + '/Boilerplate.png') + bp_bytes = boilerplate.tobytes('raw', 'RGBX') + width = boilerplate.width + height = boilerplate.height + def setUp(self): - self.width = 1024 - self.height = 1024 self.texture = quicktex.RawTexture(self.width, self.height) - self.boilerplate = Image.open(images.image_path + '/Boilerplate.png') def test_size(self): - """test byte size and image dimensions""" + """Test byte size and image dimensions""" self.assertEqual(self.texture.size, self.width * self.height * 4, "incorrect texture byte size") self.assertEqual(self.texture.width, self.width, "incorrect texture width") self.assertEqual(self.texture.height, self.height, "incorrect texture height") self.assertEqual(self.texture.dimensions, (self.width, self.height), "incorrect texture dimension tuple") def test_pixels(self): - """Test get_ and set_pixel methods for RawTexture""" + """Test getting and setting pixel values""" color1 = (69, 13, 12, 0) # totally random color color2 = (19, 142, 93, 44) @@ -33,22 +35,26 @@ class TestRawTexture(unittest.TestCase): self.assertEqual(self.texture[-1, -1], color2) self.assertEqual(tuple(data[0:4]), color1) self.assertEqual(tuple(data[-4:]), color2) + with self.assertRaises(IndexError): + thing = self.texture[self.width, self.height] + with self.assertRaises(IndexError): + thing = self.texture[-1 - self.width, -1 - self.height] def test_buffer(self): """Test the Buffer protocol implementation for RawTexture""" with tempfile.TemporaryFile('r+b') as fp: - fp.write(self.boilerplate.tobytes('raw', 'RGBX')) + fp.write(self.bp_bytes) fp.seek(0) bytes_read = fp.readinto(self.texture) - self.assertEqual(bytes_read, self.texture.size, 'buffer over/underrun') - self.assertEqual(self.boilerplate.tobytes('raw', 'RGBX'), self.texture.tobytes(), 'Incorrect bytes after writing to buffer') - self.assertEqual(bytes(self.texture), self.texture.tobytes(), "Incorrect bytes reading from buffer") + self.assertEqual(bytes_read, self.texture.size, 'buffer over/underrun') + self.assertEqual(self.bp_bytes, self.texture.tobytes(), 'Incorrect bytes after writing to buffer') + self.assertEqual(self.bp_bytes, bytes(self.texture), "Incorrect bytes after reading from buffer") - def test_factory(self): + def test_frombytes(self): """Test the frombytes factory function""" - bytetex = quicktex.RawTexture.frombytes(self.boilerplate.tobytes('raw', 'RGBX'), *self.boilerplate.size) - self.assertEqual(self.boilerplate.tobytes('raw', 'RGBX'), bytetex.tobytes(), 'Incorrect bytes after writing to buffer') + bytetex = quicktex.RawTexture.frombytes(self.bp_bytes, *self.boilerplate.size) + self.assertEqual(self.bp_bytes, bytetex.tobytes(), 'Incorrect bytes after writing to buffer') if __name__ == '__main__':