From dbc685091a6430a04b68109b96a9491217005bb9 Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Wed, 6 Sep 2023 15:13:47 +0700 Subject: [PATCH 1/4] Python: Remove numpy requirement, use buffer protocol instead Partly taken from https://github.com/zxing-cpp/zxing-cpp/issues/283#issuecomment-1517021218 This removes the requirement of `read_barcode` / `write_barcode` to have Numpy installed and uses [buffer protocol](https://pybind11.readthedocs.io/en/stable/advanced/pycpp/numpy.html#buffer-protocol) instead. Numpy arrays can already be converted to a buffer, for PIL images we get the `__array_interface__` and use its values to create a memory view with the right shape. The `write_barcode` function now returns a buffer instead of a Numpy array. This buffer also supports `__array_interface__` for easy conversion to PIL images or Numpy arrays. * For PIL: `img = Image.fromarray(result, "L")` * For Numpy: `img = np.array(result)` fixes #283 Co-authored-by: axxel --- wrappers/python/setup.py | 1 - wrappers/python/test.py | 48 +++++++++-- wrappers/python/zxing.cpp | 165 +++++++++++++++++++++++++++++--------- 3 files changed, 172 insertions(+), 42 deletions(-) diff --git a/wrappers/python/setup.py b/wrappers/python/setup.py index 232b5a731f..370b2e522c 100644 --- a/wrappers/python/setup.py +++ b/wrappers/python/setup.py @@ -76,7 +76,6 @@ def build_extension(self, ext): "Topic :: Multimedia :: Graphics", ], python_requires=">=3.6", - install_requires=["numpy"], ext_modules=[CMakeExtension('zxingcpp')], cmdclass=dict(build_ext=CMakeBuild), zip_safe=False, diff --git a/wrappers/python/test.py b/wrappers/python/test.py index 5d7f8cf30e..52e465610a 100644 --- a/wrappers/python/test.py +++ b/wrappers/python/test.py @@ -1,5 +1,6 @@ import importlib.util import unittest +import math import zxingcpp @@ -16,7 +17,6 @@ def test_format(self): self.assertEqual(zxingcpp.barcode_formats_from_str('ITF, qrcode'), BF.ITF | BF.QRCode) -@unittest.skipIf(not has_numpy, "need numpy for read/write tests") class TestReadWrite(unittest.TestCase): def check_res(self, res, format, text): @@ -60,7 +60,19 @@ def test_write_read_multi_cycle(self): res = zxingcpp.read_barcodes(img)[0] self.check_res(res, format, text) - def test_failed_read(self): + @staticmethod + def zeroes(shape): + return memoryview(b"0" * math.prod(shape)).cast("B", shape=shape) + + def test_failed_read_buffer(self): + res = zxingcpp.read_barcode( + self.zeroes((100, 100)), formats=BF.EAN8 | BF.Aztec, binarizer=zxingcpp.Binarizer.BoolCast + ) + + self.assertEqual(res, None) + + @unittest.skipIf(not has_numpy, "need numpy for read/write tests") + def test_failed_read_numpy(self): import numpy as np res = zxingcpp.read_barcode( np.zeros((100, 100), np.uint8), formats=BF.EAN8 | BF.Aztec, binarizer=zxingcpp.Binarizer.BoolCast @@ -68,6 +80,24 @@ def test_failed_read(self): self.assertEqual(res, None) + def test_write_read_cycle_buffer(self): + format = BF.QRCode + text = "I have the best words." + img = zxingcpp.write_barcode(format, text) + + self.check_res(zxingcpp.read_barcode(img), format, text) + + @unittest.skipIf(not has_numpy, "need numpy for read/write tests") + def test_write_read_cycle_numpy(self): + import numpy as np + format = BF.QRCode + text = "I have the best words." + img = zxingcpp.write_barcode(format, text) + img = np.array(img) + #img = np.ndarray(img.shape, dtype=np.uint8, buffer=img) + + self.check_res(zxingcpp.read_barcode(img), format, text) + @unittest.skipIf(not has_pil, "need PIL for read/write tests") def test_write_read_cycle_pil(self): from PIL import Image @@ -75,6 +105,7 @@ def test_write_read_cycle_pil(self): text = "I have the best words." img = zxingcpp.write_barcode(format, text) img = Image.fromarray(img, "L") + #img = Image.frombuffer("L", img.shape, img) self.check_res(zxingcpp.read_barcode(img), format, text) self.check_res(zxingcpp.read_barcode(img.convert("RGB")), format, text) @@ -84,13 +115,20 @@ def test_write_read_cycle_pil(self): def test_read_invalid_type(self): self.assertRaisesRegex( - TypeError, "Could not convert to numpy array.", zxingcpp.read_barcode, "foo" + TypeError, "Could not convert to buffer.", zxingcpp.read_barcode, "foo" + ) + + def test_read_invalid_numpy_array_channels_buffer(self): + self.assertRaisesRegex( + ValueError, "Unsupported number of channels for buffer: 4", zxingcpp.read_barcode, + self.zeroes((100, 100, 4)) ) - def test_read_invalid_numpy_array_channels(self): + @unittest.skipIf(not has_numpy, "need numpy for read/write tests") + def test_read_invalid_numpy_array_channels_numpy(self): import numpy as np self.assertRaisesRegex( - ValueError, "Unsupported number of channels for numpy array: 4", zxingcpp.read_barcode, + ValueError, "Unsupported number of channels for buffer: 4", zxingcpp.read_barcode, np.zeros((100, 100, 4), np.uint8) ) diff --git a/wrappers/python/zxing.cpp b/wrappers/python/zxing.cpp index ab615477f1..e87b19c5e9 100644 --- a/wrappers/python/zxing.cpp +++ b/wrappers/python/zxing.cpp @@ -15,19 +15,18 @@ #include "BitMatrix.h" #include "MultiFormatWriter.h" -#include #include #include #include #include #include +#include +#include +#include using namespace ZXing; namespace py = pybind11; -// Numpy array wrapper class for images (either BGR or GRAYSCALE) -using Image = py::array_t; - std::ostream& operator<<(std::ostream& os, const Position& points) { for (const auto& p : points) os << p.x << "x" << p.y << " "; @@ -49,36 +48,80 @@ auto read_barcodes_impl(py::object _image, const BarcodeFormats& formats, bool t .setMaxNumberOfSymbols(max_number_of_symbols) .setEanAddOnSymbol(ean_add_on_symbol); const auto _type = std::string(py::str(py::type::of(_image))); - Image image; + py::buffer buffer; ImageFormat imgfmt = ImageFormat::None; try { - if (_type.find("PIL.") != std::string::npos) { - _image.attr("load")(); - const auto mode = _image.attr("mode").cast(); - if (mode == "L") - imgfmt = ImageFormat::Lum; - else if (mode == "RGB") - imgfmt = ImageFormat::RGB; - else if (mode == "RGBA") - imgfmt = ImageFormat::RGBX; - else { - // Unsupported mode in ImageFormat. Let's do conversion to L mode with PIL. - _image = _image.attr("convert")("L"); - imgfmt = ImageFormat::Lum; + if (py::hasattr(_image, "__array_interface__")) { + if (_type.find("PIL.") != std::string::npos) { + _image.attr("load")(); + const auto mode = _image.attr("mode").cast(); + if (mode == "L") + imgfmt = ImageFormat::Lum; + else if (mode == "RGB") + imgfmt = ImageFormat::RGB; + else if (mode == "RGBA") + imgfmt = ImageFormat::RGBX; + else { + // Unsupported mode in ImageFormat. Let's do conversion to L mode with PIL. + _image = _image.attr("convert")("L"); + imgfmt = ImageFormat::Lum; + } + } + + auto ai = _image.attr("__array_interface__").cast(); + auto ashape = ai["shape"].cast(); + + if (ai.contains("data")) { + auto adata = ai["data"]; + + if (py::isinstance(adata)) { + auto data_tuple = adata.cast(); + auto ashape_std = ashape.cast>(); + auto data_len = Reduce(ashape_std, py::size_t(1u), std::multiplies()); + buffer = py::memoryview::from_memory(reinterpret_cast(data_tuple[0].cast()), data_len, true); + } else if (py::isinstance(adata)) { + // Numpy and our own __array_interface__ passes data as a buffer/bytes object + buffer = adata.cast(); + } else { + throw py::type_error("No way to get data from __array_interface__"); + } + } else { + buffer = _image.cast(); + } + + py::tuple bshape; + if (py::hasattr(buffer, "shape")) { + bshape = buffer.attr("shape").cast(); } + + if (!ashape.equal(bshape)) { + auto bufferview = py::memoryview(buffer); + buffer = bufferview.attr("cast")("B", ashape).cast(); + } + } else { + buffer = _image.cast(); } - image = _image.cast(); #if PYBIND11_VERSION_HEX > 0x02080000 // py::raise_from is available starting from 2.8.0 } catch (py::error_already_set &e) { - py::raise_from(e, PyExc_TypeError, ("Could not convert " + _type + " to numpy array of dtype 'uint8'.").c_str()); + py::raise_from(e, PyExc_TypeError, ("Could not convert " + _type + " to buffer.").c_str()); throw py::error_already_set(); #endif } catch (...) { - throw py::type_error("Could not convert " + _type + " to numpy array. Expecting a PIL Image or numpy array."); + throw py::type_error("Could not convert " + _type + " to buffer. Expecting a PIL Image or buffer."); } - const auto height = narrow_cast(image.shape(0)); - const auto width = narrow_cast(image.shape(1)); - const auto channels = image.ndim() == 2 ? 1 : narrow_cast(image.shape(2)); + + /* Request a buffer descriptor from Python */ + py::buffer_info info = buffer.request(); + + if (info.format != py::format_descriptor::format()) + throw py::type_error("Incompatible format: expected a uint8_t array."); + + if (info.ndim != 2 && info.ndim != 3) + throw py::type_error("Incompatible buffer dimension."); + + const auto height = narrow_cast(info.shape[0]); + const auto width = narrow_cast(info.shape[1]); + const auto channels = info.ndim == 2 ? 1 : narrow_cast(info.shape[2]); if (imgfmt == ImageFormat::None) { // Assume grayscale or BGR image depending on channels number if (channels == 1) @@ -86,10 +129,10 @@ auto read_barcodes_impl(py::object _image, const BarcodeFormats& formats, bool t else if (channels == 3) imgfmt = ImageFormat::BGR; else - throw py::value_error("Unsupported number of channels for numpy array: " + std::to_string(channels)); + throw py::value_error("Unsupported number of channels for buffer: " + std::to_string(channels)); } - const auto bytes = image.data(); + const auto bytes = static_cast(info.ptr); // Disables the GIL during zxing processing (restored automatically upon completion) py::gil_scoped_release release; return ReadBarcodes({bytes, width, height, imgfmt, width * channels, channels}, hints); @@ -108,17 +151,60 @@ Results read_barcodes(py::object _image, const BarcodeFormats& formats, bool try return read_barcodes_impl(_image, formats, try_rotate, try_downscale, text_mode, binarizer, is_pure, ean_add_on_symbol); } -Image write_barcode(BarcodeFormat format, std::string text, int width, int height, int quiet_zone, int ec_level) +class WriteResult +{ +public: + WriteResult(std::vector&& result_data_, std::vector&& shape_) + { + result_data = result_data_; + shape = shape_; + ndim = shape.size(); + } + + WriteResult(WriteResult const& o) : ndim(o.ndim), shape(o.shape), result_data(o.result_data) {} + + const std::vector& get_result_data() const { return result_data; } + + static py::buffer_info get_buffer(const WriteResult& wr) + { + return py::buffer_info( + reinterpret_cast(const_cast(wr.result_data.data())), + sizeof(char), "B", wr.shape.size(), wr.shape, { wr.shape[0], py::ssize_t(1) } + ); + } + + const std::vector get_shape() const { return shape; } + +private: + py::ssize_t ndim; + std::vector shape; + std::vector result_data; +}; + +py::object write_barcode(BarcodeFormat format, std::string text, int width, int height, int quiet_zone, int ec_level) { auto writer = MultiFormatWriter(format).setEncoding(CharacterSet::UTF8).setMargin(quiet_zone).setEccLevel(ec_level); auto bitmap = writer.encode(text, width, height); - auto result = Image({bitmap.height(), bitmap.width()}); - auto r = result.mutable_unchecked<2>(); - for (py::ssize_t y = 0; y < r.shape(0); y++) - for (py::ssize_t x = 0; x < r.shape(1); x++) - r(y, x) = bitmap.get(narrow_cast(x), narrow_cast(y)) ? 0 : 255; - return result; + std::vector result_data(bitmap.width() * bitmap.height()); + for (py::ssize_t y = 0; y < bitmap.height(); y++) + for (py::ssize_t x = 0; x < bitmap.width(); x++) + result_data[x + (y * bitmap.width())] = bitmap.get(narrow_cast(x), narrow_cast(y)) ? 0 : 255; + + WriteResult res(std::move(result_data), {bitmap.height(), bitmap.width()}); + + auto resobj = py::cast(res); + + // We add an __array_interface__ here to make the returned type easily convertible to PIL images, + // Numpy arrays and other libraries that spport the interface. + py::dict array_interface; + array_interface["version"] = py::cast(3); + array_interface["data"] = resobj; + array_interface["shape"] = res.get_shape(); + array_interface["typestr"] = py::cast("|u1"); + resobj.attr("__array_interface__") = array_interface; + + return resobj; } @@ -213,6 +299,10 @@ PYBIND11_MODULE(zxingcpp, m) oss << pos; return oss.str(); }); + py::class_(m, "WriteResult", "Result of barcode writing", py::buffer_protocol(), py::dynamic_attr()) + .def_property_readonly("shape", &WriteResult::get_shape, + ":rtype: tuple") + .def_buffer(&WriteResult::get_buffer); py::class_(m, "Result", "Result of barcode reading") .def_property_readonly("valid", &Result::isValid, ":return: whether or not result is valid (i.e. a symbol was found)\n" @@ -265,8 +355,9 @@ PYBIND11_MODULE(zxingcpp, m) py::arg("is_pure") = false, py::arg("ean_add_on_symbol") = EanAddOnSymbol::Ignore, "Read (decode) a barcode from a numpy BGR or grayscale image array or from a PIL image.\n\n" - ":type image: numpy.ndarray|PIL.Image.Image\n" + ":type image: buffer|numpy.ndarray|PIL.Image.Image\n" ":param image: The image object to decode. The image can be either:\n" + " - a buffer with the correct shape, use .cast on memory view to convert\n" " - a numpy array containing image either in grayscale (1 byte per pixel) or BGR mode (3 bytes per pixel)\n" " - a PIL Image\n" ":type formats: zxing.BarcodeFormat|zxing.BarcodeFormats\n" @@ -302,8 +393,9 @@ PYBIND11_MODULE(zxingcpp, m) py::arg("is_pure") = false, py::arg("ean_add_on_symbol") = EanAddOnSymbol::Ignore, "Read (decode) multiple barcodes from a numpy BGR or grayscale image array or from a PIL image.\n\n" - ":type image: numpy.ndarray|PIL.Image.Image\n" + ":type image: buffer|numpy.ndarray|PIL.Image.Image\n" ":param image: The image object to decode. The image can be either:\n" + " - a buffer with the correct shape, use .cast on memory view to convert\n" " - a numpy array containing image either in grayscale (1 byte per pixel) or BGR mode (3 bytes per pixel)\n" " - a PIL Image\n" ":type formats: zxing.BarcodeFormat|zxing.BarcodeFormats\n" @@ -336,7 +428,7 @@ PYBIND11_MODULE(zxingcpp, m) py::arg("height") = 0, py::arg("quiet_zone") = -1, py::arg("ec_level") = -1, - "Write (encode) a text into a barcode and return numpy (grayscale) image array\n\n" + "Write (encode) a text into a barcode and return 8-bit grayscale bitmap buffer\n\n" ":type format: zxing.BarcodeFormat\n" ":param format: format of the barcode to create\n" ":type text: str\n" @@ -353,5 +445,6 @@ PYBIND11_MODULE(zxingcpp, m) ":type ec_level: int\n" ":param ec_level: error correction level of the barcode\n" " (Used for Aztec, PDF417, and QRCode only)." + ":rtype: zxing.WriteResult\n" ); } From f36d27894c85969b74f1df4bad28a35e470cbb5c Mon Sep 17 00:00:00 2001 From: axxel Date: Thu, 14 Sep 2023 10:26:42 +0200 Subject: [PATCH 2/4] python: `Matrix` based `write_barcode` return type --- wrappers/python/zxing.cpp | 95 +++++++++++++-------------------------- 1 file changed, 31 insertions(+), 64 deletions(-) diff --git a/wrappers/python/zxing.cpp b/wrappers/python/zxing.cpp index e87b19c5e9..8f386192aa 100644 --- a/wrappers/python/zxing.cpp +++ b/wrappers/python/zxing.cpp @@ -13,19 +13,20 @@ // Writer #include "BitMatrix.h" +#include "Matrix.h" #include "MultiFormatWriter.h" #include #include #include +#include #include -#include #include -#include -#include +#include using namespace ZXing; namespace py = pybind11; +using namespace pybind11::literals; // to bring in the `_a` literal std::ostream& operator<<(std::ostream& os, const Position& points) { for (const auto& p : points) @@ -75,10 +76,9 @@ auto read_barcodes_impl(py::object _image, const BarcodeFormats& formats, bool t auto adata = ai["data"]; if (py::isinstance(adata)) { - auto data_tuple = adata.cast(); - auto ashape_std = ashape.cast>(); - auto data_len = Reduce(ashape_std, py::size_t(1u), std::multiplies()); - buffer = py::memoryview::from_memory(reinterpret_cast(data_tuple[0].cast()), data_len, true); + auto data_ptr = adata.cast()[0].cast(); + auto data_len = Reduce(ashape.cast>(), 1, std::multiplies{}); + buffer = py::memoryview::from_memory(reinterpret_cast(data_ptr), data_len, true); } else if (py::isinstance(adata)) { // Numpy and our own __array_interface__ passes data as a buffer/bytes object buffer = adata.cast(); @@ -114,10 +114,10 @@ auto read_barcodes_impl(py::object _image, const BarcodeFormats& formats, bool t py::buffer_info info = buffer.request(); if (info.format != py::format_descriptor::format()) - throw py::type_error("Incompatible format: expected a uint8_t array."); + throw py::type_error("Incompatible buffer format: expected a uint8_t array."); if (info.ndim != 2 && info.ndim != 3) - throw py::type_error("Incompatible buffer dimension."); + throw py::type_error("Incompatible buffer dimension (needs to be 2 or 3)."); const auto height = narrow_cast(info.shape[0]); const auto width = narrow_cast(info.shape[1]); @@ -151,60 +151,11 @@ Results read_barcodes(py::object _image, const BarcodeFormats& formats, bool try return read_barcodes_impl(_image, formats, try_rotate, try_downscale, text_mode, binarizer, is_pure, ean_add_on_symbol); } -class WriteResult -{ -public: - WriteResult(std::vector&& result_data_, std::vector&& shape_) - { - result_data = result_data_; - shape = shape_; - ndim = shape.size(); - } - - WriteResult(WriteResult const& o) : ndim(o.ndim), shape(o.shape), result_data(o.result_data) {} - - const std::vector& get_result_data() const { return result_data; } - - static py::buffer_info get_buffer(const WriteResult& wr) - { - return py::buffer_info( - reinterpret_cast(const_cast(wr.result_data.data())), - sizeof(char), "B", wr.shape.size(), wr.shape, { wr.shape[0], py::ssize_t(1) } - ); - } - - const std::vector get_shape() const { return shape; } - -private: - py::ssize_t ndim; - std::vector shape; - std::vector result_data; -}; - -py::object write_barcode(BarcodeFormat format, std::string text, int width, int height, int quiet_zone, int ec_level) +Matrix write_barcode(BarcodeFormat format, std::string text, int width, int height, int quiet_zone, int ec_level) { auto writer = MultiFormatWriter(format).setEncoding(CharacterSet::UTF8).setMargin(quiet_zone).setEccLevel(ec_level); auto bitmap = writer.encode(text, width, height); - - std::vector result_data(bitmap.width() * bitmap.height()); - for (py::ssize_t y = 0; y < bitmap.height(); y++) - for (py::ssize_t x = 0; x < bitmap.width(); x++) - result_data[x + (y * bitmap.width())] = bitmap.get(narrow_cast(x), narrow_cast(y)) ? 0 : 255; - - WriteResult res(std::move(result_data), {bitmap.height(), bitmap.width()}); - - auto resobj = py::cast(res); - - // We add an __array_interface__ here to make the returned type easily convertible to PIL images, - // Numpy arrays and other libraries that spport the interface. - py::dict array_interface; - array_interface["version"] = py::cast(3); - array_interface["data"] = resobj; - array_interface["shape"] = res.get_shape(); - array_interface["typestr"] = py::cast("|u1"); - resobj.attr("__array_interface__") = array_interface; - - return resobj; + return ToMatrix(bitmap); } @@ -299,10 +250,6 @@ PYBIND11_MODULE(zxingcpp, m) oss << pos; return oss.str(); }); - py::class_(m, "WriteResult", "Result of barcode writing", py::buffer_protocol(), py::dynamic_attr()) - .def_property_readonly("shape", &WriteResult::get_shape, - ":rtype: tuple") - .def_buffer(&WriteResult::get_buffer); py::class_(m, "Result", "Result of barcode reading") .def_property_readonly("valid", &Result::isValid, ":return: whether or not result is valid (i.e. a symbol was found)\n" @@ -421,6 +368,26 @@ PYBIND11_MODULE(zxingcpp, m) ":rtype: zxing.Result\n" ":return: a list of zxing results containing decoded symbols, the list is empty if none is found" ); + + py::class_>(m, "Bitmap", py::buffer_protocol()) + .def_property_readonly( + "__array_interface__", + [](const Matrix& m) { + return py::dict("version"_a = 3, "data"_a = m, "shape"_a = py::make_tuple(m.height(), m.width()), "typestr"_a = "|u1"); + }) + .def_property_readonly("shape", [](const Matrix& m) { return py::make_tuple(m.height(), m.width()); }) + .def_buffer([](const Matrix& m) -> py::buffer_info { + return { + const_cast(m.data()), // Pointer to buffer + sizeof(uint8_t), // Size of one scalar + py::format_descriptor::format(), // Python struct-style format descriptor + 2, // Number of dimensions + {m.height(), m.width()}, // Buffer dimensions + {sizeof(uint8_t) * m.width(), sizeof(uint8_t)}, // Strides (in bytes) for each index + true // read-only + }; + }); + m.def("write_barcode", &write_barcode, py::arg("format"), py::arg("text"), From d9b3e5d00daf16bb663f92f561edd8b07771cae3 Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Thu, 14 Sep 2023 15:40:01 +0700 Subject: [PATCH 3/4] Python: Add comment explaining the ashape / bshape equality test --- wrappers/python/zxing.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wrappers/python/zxing.cpp b/wrappers/python/zxing.cpp index 8f386192aa..ed0032fe4a 100644 --- a/wrappers/python/zxing.cpp +++ b/wrappers/python/zxing.cpp @@ -94,6 +94,11 @@ auto read_barcodes_impl(py::object _image, const BarcodeFormats& formats, bool t bshape = buffer.attr("shape").cast(); } + // We need to check if the shape is equal because memoryviews can only be cast from 1D + // to ND and in reverse, not from ND to ND. If the shape is already correct, as with our + // return value from write_barcode, we don't need to cast. There are libraries (PIL for + // example) that pass 1D data here, in that case we need to cast because the later code + // expects a buffer in the correct shape. if (!ashape.equal(bshape)) { auto bufferview = py::memoryview(buffer); buffer = bufferview.attr("cast")("B", ashape).cast(); From 266e4753cb14fcdafc424437ce1e1518bd57f0d9 Mon Sep 17 00:00:00 2001 From: axxel Date: Thu, 14 Sep 2023 10:59:38 +0200 Subject: [PATCH 4/4] python: updated error message and docstring --- wrappers/python/test.py | 4 +--- wrappers/python/zxing.cpp | 9 ++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/wrappers/python/test.py b/wrappers/python/test.py index 52e465610a..266b1f57fe 100644 --- a/wrappers/python/test.py +++ b/wrappers/python/test.py @@ -94,7 +94,6 @@ def test_write_read_cycle_numpy(self): text = "I have the best words." img = zxingcpp.write_barcode(format, text) img = np.array(img) - #img = np.ndarray(img.shape, dtype=np.uint8, buffer=img) self.check_res(zxingcpp.read_barcode(img), format, text) @@ -105,7 +104,6 @@ def test_write_read_cycle_pil(self): text = "I have the best words." img = zxingcpp.write_barcode(format, text) img = Image.fromarray(img, "L") - #img = Image.frombuffer("L", img.shape, img) self.check_res(zxingcpp.read_barcode(img), format, text) self.check_res(zxingcpp.read_barcode(img.convert("RGB")), format, text) @@ -115,7 +113,7 @@ def test_write_read_cycle_pil(self): def test_read_invalid_type(self): self.assertRaisesRegex( - TypeError, "Could not convert to buffer.", zxingcpp.read_barcode, "foo" + TypeError, "Invalid input: does not support the buffer protocol.", zxingcpp.read_barcode, "foo" ) def test_read_invalid_numpy_array_channels_buffer(self): diff --git a/wrappers/python/zxing.cpp b/wrappers/python/zxing.cpp index ed0032fe4a..e109141bf1 100644 --- a/wrappers/python/zxing.cpp +++ b/wrappers/python/zxing.cpp @@ -108,11 +108,11 @@ auto read_barcodes_impl(py::object _image, const BarcodeFormats& formats, bool t } #if PYBIND11_VERSION_HEX > 0x02080000 // py::raise_from is available starting from 2.8.0 } catch (py::error_already_set &e) { - py::raise_from(e, PyExc_TypeError, ("Could not convert " + _type + " to buffer.").c_str()); + py::raise_from(e, PyExc_TypeError, ("Invalid input: " + _type + " does not support the buffer protocol.").c_str()); throw py::error_already_set(); #endif } catch (...) { - throw py::type_error("Could not convert " + _type + " to buffer. Expecting a PIL Image or buffer."); + throw py::type_error("Invalid input: " + _type + " does not support the buffer protocol."); } /* Request a buffer descriptor from Python */ @@ -415,8 +415,7 @@ PYBIND11_MODULE(zxingcpp, m) ":param quiet_zone: minimum size (in pixels) of the quiet zone around barcode. If undefined (or set to -1), \n" " the minimum quiet zone of respective barcode is used." ":type ec_level: int\n" - ":param ec_level: error correction level of the barcode\n" - " (Used for Aztec, PDF417, and QRCode only)." - ":rtype: zxing.WriteResult\n" + ":param ec_level: error correction level of the barcode (Used for Aztec, PDF417, and QRCode only).\n" + ":rtype: zxing.Bitmap\n" ); }