Skip to content

Commit d1fcdfa

Browse files
authored
Merge pull request #3 from dev0x13/release-gil-on-image-decoding
Release GIL on image decoder invocations
2 parents 7910890 + 7b90102 commit d1fcdfa

4 files changed

Lines changed: 116 additions & 44 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.idea/
2+
.vscode/
23
.cache/
34
cmake-build-debug/
45
cmake-build-release/

src/wuffs-aux-image-wrapper.h

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
#pragma once
22

3-
#include <pybind11/numpy.h>
4-
53
#include <map>
64
#include <string>
75
#include <unordered_set>
@@ -138,8 +136,8 @@ struct ImageDecoderConfig {
138136
uint64_t max_incl_metadata_length =
139137
wuffs_aux::DecodeImageArgMaxInclMetadataLength::DefaultValue().repr;
140138
std::vector<ImageDecoderType> enabled_decoders = {
141-
ImageDecoderType::BMP, ImageDecoderType::GIF, ImageDecoderType::NIE,
142-
ImageDecoderType::PNG, ImageDecoderType::TGA, ImageDecoderType::WBMP,
139+
ImageDecoderType::BMP, ImageDecoderType::GIF, ImageDecoderType::NIE,
140+
ImageDecoderType::PNG, ImageDecoderType::TGA, ImageDecoderType::WBMP,
143141
ImageDecoderType::JPEG, ImageDecoderType::WEBP, ImageDecoderType::QOI,
144142
ImageDecoderType::ETC2, ImageDecoderType::TH};
145143
uint32_t pixel_format = wuffs_base__make_pixel_format(
@@ -151,10 +149,10 @@ struct ImageDecoderConfig {
151149
// input
152150
struct MetadataEntry {
153151
wuffs_base__more_information minfo{};
154-
pybind11::array_t<uint8_t> data;
152+
std::vector<uint8_t> data;
155153

156154
MetadataEntry(const wuffs_base__more_information& minfo,
157-
pybind11::array_t<uint8_t>&& data)
155+
std::vector<uint8_t>&& data)
158156
: minfo(minfo), data(std::move(data)) {}
159157

160158
MetadataEntry() : minfo(wuffs_base__empty_more_information()) {}
@@ -178,7 +176,7 @@ struct MetadataEntry {
178176

179177
struct ImageDecodingResult {
180178
wuffs_base__pixel_config pixcfg = wuffs_base__null_pixel_config();
181-
pybind11::array_t<uint8_t> pixbuf;
179+
std::vector<uint8_t> pixbuf;
182180
std::vector<MetadataEntry> reported_metadata;
183181
std::string error_message;
184182

@@ -274,7 +272,7 @@ class ImageDecoder : public wuffs_aux::DecodeImageCallbacks {
274272
std::string HandleMetadata(const wuffs_base__more_information& minfo,
275273
wuffs_base__slice_u8 raw) override {
276274
decoding_result_.reported_metadata.emplace_back(
277-
minfo, pybind11::array(pybind11::dtype("uint8"), {raw.len}, raw.ptr));
275+
minfo, std::vector<uint8_t>{raw.ptr, raw.ptr + raw.len});
278276
return "";
279277
}
280278

@@ -296,15 +294,15 @@ class ImageDecoder : public wuffs_aux::DecodeImageCallbacks {
296294
if (len == 0 || SIZE_MAX < len) {
297295
return {wuffs_aux::DecodeImage_UnsupportedPixelConfiguration};
298296
}
299-
decoding_result_.pixbuf.resize({len});
297+
decoding_result_.pixbuf.resize(len);
300298
if (!allow_uninitialized_memory) {
301-
std::memset(decoding_result_.pixbuf.mutable_data(), 0,
299+
std::memset(decoding_result_.pixbuf.data(), 0,
302300
decoding_result_.pixbuf.size());
303301
}
304302
wuffs_base__pixel_buffer pixbuf;
305303
wuffs_base__status status = pixbuf.set_from_slice(
306304
&image_config.pixcfg,
307-
wuffs_base__make_slice_u8(decoding_result_.pixbuf.mutable_data(),
305+
wuffs_base__make_slice_u8(decoding_result_.pixbuf.data(),
308306
decoding_result_.pixbuf.size()));
309307
if (!status.is_ok()) {
310308
decoding_result_.pixbuf = {};
@@ -353,12 +351,6 @@ class ImageDecoder : public wuffs_aux::DecodeImageCallbacks {
353351
decoding_result_.pixcfg = wuffs_base__null_pixel_config();
354352
} else {
355353
decoding_result_.pixcfg = decode_image_result.pixbuf.pixcfg;
356-
decoding_result_.pixbuf =
357-
decoding_result_.pixbuf.reshape(std::vector<size_t>{
358-
decoding_result_.pixcfg.height(), decoding_result_.pixcfg.width(),
359-
decoding_result_.pixcfg.pixbuf_len() /
360-
(decoding_result_.pixcfg.width() *
361-
decoding_result_.pixcfg.height())});
362354
}
363355
return std::move(decoding_result_);
364356
}

src/wuffs-bindings.cpp

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,7 @@ PYBIND11_MODULE(pywuffs, m) {
8282
wuffs_aux_wrap::ImageDecoderQuirks::GIF_REJECT_EMPTY_FRAME)
8383
.value("GIF_REJECT_EMPTY_PALETTE",
8484
wuffs_aux_wrap::ImageDecoderQuirks::GIF_REJECT_EMPTY_PALETTE)
85-
.value("QUALITY",
86-
wuffs_aux_wrap::ImageDecoderQuirks::QUALITY,
85+
.value("QUALITY", wuffs_aux_wrap::ImageDecoderQuirks::QUALITY,
8786
"Configures decoders (for a lossy format, where there is some "
8887
"leeway in \"a/the correct decoding\") to use lower than, equal "
8988
"to or higher than the default quality setting.");
@@ -216,8 +215,15 @@ py::enum_<wuffs_aux_wrap::PixelFormat>(
216215
"Holds parsed metadata piece.")
217216
.def_readonly("minfo", &wuffs_aux_wrap::MetadataEntry::minfo,
218217
"wuffs_base__more_information: Info on parsed metadata.")
219-
.def_readonly("data", &wuffs_aux_wrap::MetadataEntry::data,
220-
"np.array: Parsed metadata (1D uint8 Numpy array).");
218+
.def_property_readonly(
219+
"data",
220+
[](wuffs_aux_wrap::MetadataEntry& self) {
221+
return pybind11::array_t<uint8_t>(pybind11::buffer_info(
222+
self.data.data(), sizeof(uint8_t),
223+
pybind11::format_descriptor<uint8_t>::value, 1,
224+
{self.data.size()}, {1}));
225+
},
226+
"np.array: Parsed metadata (1D uint8 Numpy array).");
221227

222228
py::class_<wuffs_aux_wrap::ImageDecoderConfig>(aux_m, "ImageDecoderConfig",
223229
"Image decoder configuration.")
@@ -319,9 +325,31 @@ py::enum_<wuffs_aux_wrap::PixelFormat>(
319325
"or reject partial success.\n"
320326
" - On failure, the error_message is non-empty and pixbuf is "
321327
"empty.")
322-
.def_readonly("pixbuf", &wuffs_aux_wrap::ImageDecodingResult::pixbuf,
323-
"np.array: decoded pixel buffer (uint8 Numpy array of [H, "
324-
"W, C] shape).")
328+
.def_property_readonly(
329+
"pixbuf",
330+
[](wuffs_aux_wrap::ImageDecodingResult& self)
331+
-> pybind11::array_t<uint8_t> {
332+
const auto height = self.pixcfg.height();
333+
const auto width = self.pixcfg.width();
334+
335+
if (width == 0 || height == 0) {
336+
return {};
337+
}
338+
339+
constexpr size_t kNumDimensions = 3;
340+
const auto channels = self.pixcfg.pixbuf_len() / (width * height);
341+
const std::array<size_t, kNumDimensions> shape = {height, width,
342+
channels};
343+
const std::array<size_t, kNumDimensions> strides = {
344+
width * channels, channels, 1};
345+
346+
return pybind11::array_t<uint8_t>(pybind11::buffer_info(
347+
self.pixbuf.data(), sizeof(uint8_t),
348+
pybind11::format_descriptor<uint8_t>::value, kNumDimensions,
349+
shape, strides));
350+
},
351+
"np.array: decoded pixel buffer (uint8 Numpy array of [H, "
352+
"W, C] shape).")
325353
.def_readonly("pixcfg", &wuffs_aux_wrap::ImageDecodingResult::pixcfg,
326354
"wuffs_base__pixel_config: decoded pixel buffer config.")
327355
.def_readonly("reported_metadata",
@@ -337,14 +365,15 @@ py::enum_<wuffs_aux_wrap::PixelFormat>(
337365
py::class_<wuffs_aux_wrap::ImageDecoder>(aux_m, "ImageDecoder",
338366
"Image decoder class.")
339367
.def(py::init<const wuffs_aux_wrap::ImageDecoderConfig&>(),
340-
"Sole constructor.\n\n"
368+
"Sole constructor. Please note that the class is not thread-safe.\n\n"
341369
"Args:"
342370
"\n config (ImageDecoderConfig): image decoder config.")
343371
.def(
344372
"decode",
345373
[](wuffs_aux_wrap::ImageDecoder& image_decoder,
346374
const py::bytes& data) -> wuffs_aux_wrap::ImageDecodingResult {
347375
py::buffer_info data_view(py::buffer(data).request());
376+
pybind11::gil_scoped_release release_gil;
348377
return image_decoder.Decode(
349378
reinterpret_cast<uint8_t*>(data_view.ptr), data_view.size);
350379
},
@@ -358,6 +387,7 @@ py::enum_<wuffs_aux_wrap::PixelFormat>(
358387
[](wuffs_aux_wrap::ImageDecoder& image_decoder,
359388
const std::string& path_to_file)
360389
-> wuffs_aux_wrap::ImageDecodingResult {
390+
pybind11::gil_scoped_release release_gil;
361391
return image_decoder.Decode(path_to_file);
362392
},
363393
"Decodes image using given file path.\n\n"

test/test_aux_image_decoder.py

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,21 @@
66
from pywuffs.aux import *
77
from pywuffs import *
88

9-
IMAGES_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "images/")
9+
IMAGES_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "images")
1010
TEST_IMAGES = [
11-
(ImageDecoderType.PNG, IMAGES_PATH + "/lena.png"),
12-
(ImageDecoderType.BMP, IMAGES_PATH + "/lena.bmp"),
13-
(ImageDecoderType.TGA, IMAGES_PATH + "/lena.tga"),
14-
(ImageDecoderType.NIE, IMAGES_PATH + "/hippopotamus.nie"),
15-
(ImageDecoderType.GIF, IMAGES_PATH + "/lena.gif"),
16-
(ImageDecoderType.WBMP, IMAGES_PATH + "/lena.wbmp"),
17-
(ImageDecoderType.JPEG, IMAGES_PATH + "/lena.jpeg"),
18-
(ImageDecoderType.WEBP, IMAGES_PATH + "/lena.webp"),
19-
(ImageDecoderType.QOI, IMAGES_PATH + "/lena.qoi"),
20-
(ImageDecoderType.ETC2, IMAGES_PATH + "/bricks-color.etc2.pkm"),
21-
(ImageDecoderType.TH, IMAGES_PATH + "/1QcSHQRnh493V4dIh4eXh1h4kJUI.th")
11+
(ImageDecoderType.PNG, os.path.join(IMAGES_PATH, "lena.png")),
12+
(ImageDecoderType.BMP, os.path.join(IMAGES_PATH, "lena.bmp")),
13+
(ImageDecoderType.TGA, os.path.join(IMAGES_PATH, "lena.tga")),
14+
(ImageDecoderType.NIE, os.path.join(IMAGES_PATH, "hippopotamus.nie")),
15+
(ImageDecoderType.GIF, os.path.join(IMAGES_PATH, "lena.gif")),
16+
(ImageDecoderType.WBMP, os.path.join(IMAGES_PATH, "lena.wbmp")),
17+
(ImageDecoderType.JPEG, os.path.join(IMAGES_PATH, "lena.jpeg")),
18+
(ImageDecoderType.WEBP, os.path.join(IMAGES_PATH, "lena.webp")),
19+
(ImageDecoderType.QOI, os.path.join(IMAGES_PATH, "lena.qoi")),
20+
(ImageDecoderType.ETC2, os.path.join(IMAGES_PATH, "bricks-color.etc2.pkm")),
21+
(ImageDecoderType.TH, os.path.join(IMAGES_PATH, "1QcSHQRnh493V4dIh4eXh1h4kJUI.th"))
2222
]
23+
EXIF_FOURCC = 0x45584946
2324

2425

2526
# Positive test cases
@@ -48,7 +49,7 @@ def test_decode_image_with_metadata(param):
4849
config = ImageDecoderConfig()
4950
config.flags = param[0]
5051
decoder = ImageDecoder(config)
51-
decoding_result = decoder.decode(IMAGES_PATH + "/lena_exif.png")
52+
decoding_result = decoder.decode(os.path.join(IMAGES_PATH, "lena_exif.png"))
5253
assert_decoded(decoding_result, param[1])
5354

5455

@@ -140,12 +141,12 @@ def test_decode_image_quirks_quality():
140141
config = ImageDecoderConfig()
141142
config.quirks = {ImageDecoderQuirks.QUALITY: LowerQuality}
142143
decoder = ImageDecoder(config)
143-
decoding_result_lower_quality = decoder.decode(IMAGES_PATH + "/lena.jpeg")
144+
decoding_result_lower_quality = decoder.decode(os.path.join(IMAGES_PATH, "lena.jpeg"))
144145
assert_decoded(decoding_result_lower_quality)
145146
assert decoding_result_lower_quality.pixbuf.shape == (32, 32, 4)
146147
config.quirks = {ImageDecoderQuirks.QUALITY: HigherQuality}
147148
decoder = ImageDecoder(config)
148-
decoding_result_higher_quality = decoder.decode(IMAGES_PATH + "/lena.jpeg")
149+
decoding_result_higher_quality = decoder.decode(os.path.join(IMAGES_PATH, "lena.jpeg"))
149150
assert_decoded(decoding_result_higher_quality)
150151
assert decoding_result_higher_quality.pixbuf.shape == (32, 32, 4)
151152
assert decoding_result_lower_quality != decoding_result_higher_quality
@@ -176,12 +177,12 @@ def test_decode_image_exif_metadata():
176177
config = ImageDecoderConfig()
177178
config.flags = [ImageDecoderFlags.REPORT_METADATA_EXIF]
178179
decoder = ImageDecoder(config)
179-
decoding_result = decoder.decode(IMAGES_PATH + "/lena_exif.png")
180+
decoding_result = decoder.decode(os.path.join(IMAGES_PATH, "lena_exif.png"))
180181
assert_decoded(decoding_result, 1)
181182
assert decoding_result.pixbuf.shape == (32, 32, 4)
182183
meta_minfo = decoding_result.reported_metadata[0].minfo
183184
meta_bytes = decoding_result.reported_metadata[0].data.tobytes()
184-
assert meta_minfo.metadata__fourcc() == 1163413830 # EXIF
185+
assert meta_minfo.metadata__fourcc() == EXIF_FOURCC
185186
assert meta_bytes[:2] == b"II" # little endian
186187
exif_orientation = 0
187188
cursor = 0
@@ -215,7 +216,7 @@ def test_decode_image_invalid_kvp_chunk():
215216
config = ImageDecoderConfig()
216217
config.flags = [ImageDecoderFlags.REPORT_METADATA_KVP]
217218
decoder = ImageDecoder(config)
218-
decoding_result = decoder.decode(IMAGES_PATH + "/lena.png")
219+
decoding_result = decoder.decode(os.path.join(IMAGES_PATH, "lena.png"))
219220
assert_not_decoded(decoding_result, "png: bad text chunk (not Latin-1)", 1)
220221

221222

@@ -284,5 +285,53 @@ def test_decode_image_max_incl_metadata_length():
284285
config.max_incl_metadata_length = 8
285286
config.flags = [ImageDecoderFlags.REPORT_METADATA_EXIF]
286287
decoder = ImageDecoder(config)
287-
decoding_result = decoder.decode(IMAGES_PATH + "/lena_exif.png")
288+
decoding_result = decoder.decode(os.path.join(IMAGES_PATH, "lena_exif.png"))
288289
assert_not_decoded(decoding_result, ImageDecoderError.MaxInclMetadataLengthExceeded)
290+
291+
292+
# Multithreading tests
293+
294+
from concurrent.futures import ThreadPoolExecutor
295+
296+
297+
def test_decode_multithreaded():
298+
config = ImageDecoderConfig()
299+
300+
def decode(image):
301+
decoder = ImageDecoder(config)
302+
return decoder.decode(image)
303+
304+
test_image = os.path.join(IMAGES_PATH, "lena.png")
305+
with open(test_image, "rb") as f:
306+
test_image_data = f.read()
307+
308+
for payload in (test_image, test_image_data):
309+
for num_threads in (1, 2, 4, 8):
310+
with ThreadPoolExecutor(max_workers=num_threads) as executor:
311+
futures = [executor.submit(decode, payload) for _ in range(num_threads)]
312+
results = [future.result() for future in futures]
313+
for result in results:
314+
assert_decoded(result)
315+
assert np.array_equal(results[0].pixbuf, result.pixbuf)
316+
317+
318+
def test_decode_multithreaded_with_metadata():
319+
config = ImageDecoderConfig()
320+
config.flags = [ImageDecoderFlags.REPORT_METADATA_EXIF]
321+
322+
def decode(image):
323+
decoder = ImageDecoder(config)
324+
return decoder.decode(image)
325+
326+
test_image = os.path.join(IMAGES_PATH, "lena_exif.png")
327+
num_threads = 4
328+
329+
with ThreadPoolExecutor(max_workers=num_threads) as executor:
330+
futures = [executor.submit(decode, test_image) for _ in range(num_threads)]
331+
results = [future.result() for future in futures]
332+
for result in results:
333+
assert_decoded(result, 1)
334+
meta_minfo = result.reported_metadata[0].minfo
335+
meta_bytes = result.reported_metadata[0].data.tobytes()
336+
assert meta_minfo.metadata__fourcc() == EXIF_FOURCC
337+
assert meta_bytes[:2] == b"II"

0 commit comments

Comments
 (0)