mirror of
https://github.com/blender/blender
synced 2026-09-29 04:37:17 +03:00
PyAPI: add ImBuf buffer methods & context manager for pixel data access
Provide buffer access to pixel data, as well as methods to ensure/clear float/byte buffers as previously it wasn't possible to create new float images. - ImBuf.with_buffer(): context manager yielding a memoryview of pixel data (byte or float, with optional region and write support). - ImBuf.ensure_buffer(), has_buffer(), clear_buffer(): manage byte/float pixel buffers. - Export blocking: resize/crop/free raise BufferError while pixel buffers are active. - PyC_ParseRectI, PyC_ParseOptionalRectI: reusable O& converters for rcti argument parsing. - Additional tests. Ref !155710
This commit is contained in:
parent
31c32237d8
commit
8cf8973f69
4 changed files with 860 additions and 0 deletions
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
#include <Python.h>
|
||||
|
||||
#include "BLI_rect.h"
|
||||
#include "BLI_string.h"
|
||||
#include "BLI_utildefines.h"
|
||||
|
||||
|
|
@ -33,6 +34,33 @@ static PyObject *BPyInit_imbuf_types();
|
|||
|
||||
static PyObject *Py_ImBuf_CreatePyObject(ImBuf *ibuf);
|
||||
|
||||
struct Py_ImBuf;
|
||||
|
||||
#define PY_IMBUF_BUFFER_TYPE_DOC \
|
||||
" :param type: The buffer type.\n" \
|
||||
" :type type: Literal['BYTE', 'FLOAT']\n"
|
||||
|
||||
static const PyC_StringEnumItems py_imbuf_buffer_mode_items[] = {
|
||||
{IB_byte_data, "BYTE"},
|
||||
{IB_float_data, "FLOAT"},
|
||||
{0, nullptr},
|
||||
};
|
||||
|
||||
struct Py_ImBufBuffer {
|
||||
PyObject_HEAD
|
||||
/** Reference to the #ImBuf this came from (prevents freeing while in use). */
|
||||
Py_ImBuf *py_ibuf;
|
||||
/** Whether this wraps byte/float pixel data (#IB_byte_data, #IB_float_data). */
|
||||
int mode;
|
||||
/** Set by `__enter__`, cleared by `__exit__` (managed #Py_ImBuf.buffer_users). */
|
||||
bool is_entered;
|
||||
/** When false the `memoryview` is read-only. */
|
||||
bool writable;
|
||||
/** Sub-region bounds (min inclusive, max exclusive). */
|
||||
std::optional<rcti> region;
|
||||
};
|
||||
extern PyTypeObject Py_ImBufBuffer_Type;
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Type & Utilities
|
||||
* \{ */
|
||||
|
|
@ -41,6 +69,8 @@ struct Py_ImBuf {
|
|||
PyObject_VAR_HEAD
|
||||
/* can be nullptr */
|
||||
ImBuf *ibuf;
|
||||
/** Number of active buffer protocol exports (via #Py_ImBufBuffer). */
|
||||
int buffer_users;
|
||||
};
|
||||
|
||||
static int py_imbuf_valid_check(Py_ImBuf *self)
|
||||
|
|
@ -65,6 +95,34 @@ static int py_imbuf_valid_check(Py_ImBuf *self)
|
|||
} \
|
||||
((void)0)
|
||||
|
||||
#define PY_IMBUF_CHECK_BUFFER_USERS(obj) \
|
||||
if (UNLIKELY((obj)->buffer_users > 0)) { \
|
||||
PyErr_SetString(PyExc_BufferError, \
|
||||
"ImBuf cannot be modified while pixel buffers are exported"); \
|
||||
return nullptr; \
|
||||
} \
|
||||
((void)0)
|
||||
|
||||
/**
|
||||
* Clamp a region to image bounds.
|
||||
*
|
||||
* \param ibuf: The image buffer (used for clamping).
|
||||
* \param region: Region to clamp in-place (inverted ranges are clamped to zero area).
|
||||
* \param r_use_region: Set to false when the region covers the entire image.
|
||||
*/
|
||||
static bool py_imbuf_region_sanitize(const ImBuf *ibuf, rcti *region)
|
||||
{
|
||||
BLI_rcti_sanitize(region);
|
||||
|
||||
CLAMP(region->xmin, 0, ibuf->x);
|
||||
CLAMP(region->xmax, 0, ibuf->x);
|
||||
CLAMP(region->ymin, 0, ibuf->y);
|
||||
CLAMP(region->ymax, 0, ibuf->y);
|
||||
|
||||
return !((region->xmin == 0 && region->ymin == 0) &&
|
||||
(region->xmax == ibuf->x && region->ymax == ibuf->y));
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
|
|
@ -85,6 +143,7 @@ PyDoc_STRVAR(
|
|||
static PyObject *py_imbuf_resize(Py_ImBuf *self, PyObject *args, PyObject *kw)
|
||||
{
|
||||
PY_IMBUF_CHECK_OBJ(self);
|
||||
PY_IMBUF_CHECK_BUFFER_USERS(self);
|
||||
|
||||
int size[2];
|
||||
|
||||
|
|
@ -141,6 +200,7 @@ PyDoc_STRVAR(
|
|||
static PyObject *py_imbuf_crop(Py_ImBuf *self, PyObject *args, PyObject *kw)
|
||||
{
|
||||
PY_IMBUF_CHECK_OBJ(self);
|
||||
PY_IMBUF_CHECK_BUFFER_USERS(self);
|
||||
|
||||
rcti crop;
|
||||
|
||||
|
|
@ -205,6 +265,233 @@ static PyObject *py_imbuf_deepcopy(Py_ImBuf *self, PyObject *args)
|
|||
return py_imbuf_copy(self);
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
py_imbuf_with_buffer_doc,
|
||||
".. method:: with_buffer(type, *, write=False, region=None)\n"
|
||||
"\n"
|
||||
" Return a context manager that yields a :class:`memoryview` "
|
||||
"of the image's pixel data.\n"
|
||||
"\n"
|
||||
" Usage::\n"
|
||||
"\n"
|
||||
" with image.with_buffer('BYTE', write=True) as buf:\n"
|
||||
" buf[0] = 255 # set red channel of first pixel\n"
|
||||
"\n" PY_IMBUF_BUFFER_TYPE_DOC
|
||||
" :param write: When true the buffer is writable.\n"
|
||||
" :type write: bool\n"
|
||||
" :param region: Optional sub-region ``((x_min, y_min), (x_max, y_max))``.\n"
|
||||
" When set the memoryview is 2D (rows x columns), "
|
||||
"clamped to image bounds.\n"
|
||||
" :type region: tuple[tuple[int, int], tuple[int, int]] | None\n"
|
||||
" :return: A context manager yielding a :class:`memoryview` of pixel data.\n"
|
||||
" :rtype: :class:`ImBufBuffer`\n");
|
||||
static PyObject *py_imbuf_with_buffer(Py_ImBuf *self, PyObject *args, PyObject *kw)
|
||||
{
|
||||
PY_IMBUF_CHECK_OBJ(self);
|
||||
|
||||
PyC_StringEnum type = {py_imbuf_buffer_mode_items, -1};
|
||||
bool write = false;
|
||||
std::optional<rcti> region;
|
||||
|
||||
static const char *_keywords[] = {
|
||||
"type",
|
||||
"write",
|
||||
"region",
|
||||
nullptr,
|
||||
};
|
||||
static _PyArg_Parser _parser = {
|
||||
"O&" /* `type` (required) */
|
||||
"|$" /* Optional keyword only arguments. */
|
||||
"O&" /* `write` */
|
||||
"O&" /* `region` */
|
||||
":with_buffer",
|
||||
_keywords,
|
||||
nullptr,
|
||||
};
|
||||
if (!_PyArg_ParseTupleAndKeywordsFast(args,
|
||||
kw,
|
||||
&_parser,
|
||||
PyC_ParseStringEnum,
|
||||
&type,
|
||||
PyC_ParseBool,
|
||||
&write,
|
||||
PyC_ParseOptionalRectI,
|
||||
®ion))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ImBuf *ibuf = self->ibuf;
|
||||
const int mode = type.value_found;
|
||||
|
||||
if (mode == IB_byte_data) {
|
||||
if (ibuf->byte_buffer.data == nullptr) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "ImBuf has no byte pixel data");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (ibuf->float_buffer.data == nullptr) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "ImBuf has no float pixel data");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool use_region = false;
|
||||
if (region.has_value()) {
|
||||
use_region = py_imbuf_region_sanitize(ibuf, ®ion.value());
|
||||
}
|
||||
|
||||
Py_ImBufBuffer *ctx = PyObject_GC_New(Py_ImBufBuffer, &Py_ImBufBuffer_Type);
|
||||
if (UNLIKELY(ctx == nullptr)) {
|
||||
return nullptr;
|
||||
}
|
||||
Py_INCREF(self);
|
||||
ctx->py_ibuf = self;
|
||||
ctx->is_entered = false;
|
||||
ctx->mode = mode;
|
||||
ctx->writable = write;
|
||||
ctx->region = use_region ? region : std::nullopt;
|
||||
return reinterpret_cast<PyObject *>(ctx);
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
py_imbuf_ensure_buffer_doc,
|
||||
".. method:: ensure_buffer(type)\n"
|
||||
"\n"
|
||||
" Ensure the image has pixel data of the given type.\n"
|
||||
" If absent, it is allocated and converted from the other buffer when available.\n"
|
||||
"\n" PY_IMBUF_BUFFER_TYPE_DOC);
|
||||
static PyObject *py_imbuf_ensure_buffer(Py_ImBuf *self, PyObject *args, PyObject *kw)
|
||||
{
|
||||
PY_IMBUF_CHECK_OBJ(self);
|
||||
PY_IMBUF_CHECK_BUFFER_USERS(self);
|
||||
|
||||
PyC_StringEnum type = {py_imbuf_buffer_mode_items, -1};
|
||||
|
||||
static const char *_keywords[] = {
|
||||
"type",
|
||||
nullptr,
|
||||
};
|
||||
static _PyArg_Parser _parser = {
|
||||
"O&" /* `type` (required) */
|
||||
":ensure_buffer",
|
||||
_keywords,
|
||||
nullptr,
|
||||
};
|
||||
if (!_PyArg_ParseTupleAndKeywordsFast(args, kw, &_parser, PyC_ParseStringEnum, &type)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ImBuf *ibuf = self->ibuf;
|
||||
|
||||
if (type.value_found == IB_byte_data) {
|
||||
if (ibuf->byte_buffer.data == nullptr) {
|
||||
if (ibuf->float_buffer.data != nullptr) {
|
||||
IMB_byte_from_float(ibuf);
|
||||
}
|
||||
else {
|
||||
IMB_alloc_byte_pixels(ibuf);
|
||||
}
|
||||
if (UNLIKELY(ibuf->byte_buffer.data == nullptr)) {
|
||||
PyErr_SetString(PyExc_MemoryError, "failed to allocate byte buffer");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (ibuf->float_buffer.data == nullptr) {
|
||||
if (ibuf->byte_buffer.data != nullptr) {
|
||||
IMB_float_from_byte(ibuf);
|
||||
}
|
||||
else {
|
||||
IMB_alloc_float_pixels(ibuf, 4);
|
||||
}
|
||||
if (UNLIKELY(ibuf->float_buffer.data == nullptr)) {
|
||||
PyErr_SetString(PyExc_MemoryError, "failed to allocate float buffer");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
py_imbuf_has_buffer_doc,
|
||||
".. method:: has_buffer(type)\n"
|
||||
"\n"
|
||||
" Return whether the image has pixel data of the given type.\n"
|
||||
"\n" PY_IMBUF_BUFFER_TYPE_DOC
|
||||
" :return: True if the buffer exists.\n"
|
||||
" :rtype: bool\n");
|
||||
static PyObject *py_imbuf_has_buffer(Py_ImBuf *self, PyObject *args, PyObject *kw)
|
||||
{
|
||||
PY_IMBUF_CHECK_OBJ(self);
|
||||
|
||||
PyC_StringEnum type = {py_imbuf_buffer_mode_items, -1};
|
||||
|
||||
static const char *_keywords[] = {
|
||||
"type",
|
||||
nullptr,
|
||||
};
|
||||
static _PyArg_Parser _parser = {
|
||||
"O&" /* `type` (required) */
|
||||
":has_buffer",
|
||||
_keywords,
|
||||
nullptr,
|
||||
};
|
||||
if (!_PyArg_ParseTupleAndKeywordsFast(args, kw, &_parser, PyC_ParseStringEnum, &type)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ImBuf *ibuf = self->ibuf;
|
||||
if (type.value_found == IB_byte_data) {
|
||||
return PyBool_FromLong(ibuf->byte_buffer.data != nullptr);
|
||||
}
|
||||
return PyBool_FromLong(ibuf->float_buffer.data != nullptr);
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
py_imbuf_clear_buffer_doc,
|
||||
".. method:: clear_buffer(type)\n"
|
||||
"\n"
|
||||
" Free pixel data of the given type.\n"
|
||||
"\n" PY_IMBUF_BUFFER_TYPE_DOC);
|
||||
static PyObject *py_imbuf_clear_buffer(Py_ImBuf *self, PyObject *args, PyObject *kw)
|
||||
{
|
||||
PY_IMBUF_CHECK_OBJ(self);
|
||||
PY_IMBUF_CHECK_BUFFER_USERS(self);
|
||||
|
||||
PyC_StringEnum type = {py_imbuf_buffer_mode_items, -1};
|
||||
|
||||
static const char *_keywords[] = {
|
||||
"type",
|
||||
nullptr,
|
||||
};
|
||||
static _PyArg_Parser _parser = {
|
||||
"O&" /* `type` (required) */
|
||||
":clear_buffer",
|
||||
_keywords,
|
||||
nullptr,
|
||||
};
|
||||
if (!_PyArg_ParseTupleAndKeywordsFast(args, kw, &_parser, PyC_ParseStringEnum, &type)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ImBuf *ibuf = self->ibuf;
|
||||
if (type.value_found == IB_byte_data) {
|
||||
IMB_free_byte_pixels(ibuf);
|
||||
}
|
||||
else {
|
||||
IMB_free_float_pixels(ibuf);
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
py_imbuf_free_doc,
|
||||
|
|
@ -213,6 +500,7 @@ PyDoc_STRVAR(
|
|||
" Clear image data immediately (causing an error on re-use).\n");
|
||||
static PyObject *py_imbuf_free(Py_ImBuf *self)
|
||||
{
|
||||
PY_IMBUF_CHECK_BUFFER_USERS(self);
|
||||
if (self->ibuf) {
|
||||
IMB_freeImBuf(self->ibuf);
|
||||
self->ibuf = nullptr;
|
||||
|
|
@ -239,6 +527,22 @@ static PyMethodDef Py_ImBuf_methods[] = {
|
|||
reinterpret_cast<PyCFunction>(py_imbuf_crop),
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
const_cast<char *>(py_imbuf_crop_doc)},
|
||||
{"with_buffer",
|
||||
reinterpret_cast<PyCFunction>(py_imbuf_with_buffer),
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
py_imbuf_with_buffer_doc},
|
||||
{"ensure_buffer",
|
||||
reinterpret_cast<PyCFunction>(py_imbuf_ensure_buffer),
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
py_imbuf_ensure_buffer_doc},
|
||||
{"has_buffer",
|
||||
reinterpret_cast<PyCFunction>(py_imbuf_has_buffer),
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
py_imbuf_has_buffer_doc},
|
||||
{"clear_buffer",
|
||||
reinterpret_cast<PyCFunction>(py_imbuf_clear_buffer),
|
||||
METH_VARARGS | METH_KEYWORDS,
|
||||
py_imbuf_clear_buffer_doc},
|
||||
{"free", reinterpret_cast<PyCFunction>(py_imbuf_free), METH_NOARGS, py_imbuf_free_doc},
|
||||
{"copy", reinterpret_cast<PyCFunction>(py_imbuf_copy), METH_NOARGS, py_imbuf_copy_doc},
|
||||
{"__copy__", reinterpret_cast<PyCFunction>(py_imbuf_copy), METH_NOARGS, py_imbuf_copy_doc},
|
||||
|
|
@ -555,11 +859,239 @@ static PyObject *Py_ImBuf_CreatePyObject(ImBuf *ibuf)
|
|||
{
|
||||
Py_ImBuf *self = PyObject_New(Py_ImBuf, &Py_ImBuf_Type);
|
||||
self->ibuf = ibuf;
|
||||
self->buffer_users = 0;
|
||||
return reinterpret_cast<PyObject *>(self);
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Buffer, Type & Implementation
|
||||
* \{ */
|
||||
|
||||
static void py_imbuf_buffer_dealloc(Py_ImBufBuffer *self)
|
||||
{
|
||||
if (self->is_entered && self->py_ibuf) {
|
||||
self->py_ibuf->buffer_users--;
|
||||
BLI_assert(self->py_ibuf->buffer_users >= 0);
|
||||
}
|
||||
PyObject_GC_UnTrack(self);
|
||||
Py_CLEAR(self->py_ibuf);
|
||||
PyObject_GC_Del(self);
|
||||
}
|
||||
|
||||
static int py_imbuf_buffer_traverse(Py_ImBufBuffer *self, visitproc visit, void *arg)
|
||||
{
|
||||
Py_VISIT(self->py_ibuf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int py_imbuf_buffer_clear(Py_ImBufBuffer *self)
|
||||
{
|
||||
Py_CLEAR(self->py_ibuf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static PyObject *py_imbuf_buffer_repr(Py_ImBufBuffer *self)
|
||||
{
|
||||
const ImBuf *ibuf = self->py_ibuf ? self->py_ibuf->ibuf : nullptr;
|
||||
if (ibuf != nullptr) {
|
||||
return PyUnicode_FromFormat(
|
||||
"<ImBufBuffer: size=(%d, %d), entered=%d>", ibuf->x, ibuf->y, int(self->is_entered));
|
||||
}
|
||||
return PyUnicode_FromString("<ImBufBuffer: invalid>");
|
||||
}
|
||||
|
||||
static PyObject *py_imbuf_buffer_enter(Py_ImBufBuffer *self)
|
||||
{
|
||||
if (UNLIKELY(self->is_entered)) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "ImBufBuffer context is already entered");
|
||||
return nullptr;
|
||||
}
|
||||
ImBuf *ibuf = self->py_ibuf->ibuf;
|
||||
if (UNLIKELY(ibuf == nullptr)) {
|
||||
PyErr_SetString(PyExc_ReferenceError, "ImBuf data has been freed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const bool is_byte = (self->mode == IB_byte_data);
|
||||
|
||||
if (is_byte) {
|
||||
if (UNLIKELY(ibuf->byte_buffer.data == nullptr)) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "ImBuf has no byte pixel data");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (UNLIKELY(ibuf->float_buffer.data == nullptr)) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "ImBuf has no float pixel data");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
const int channels = is_byte ? 4 : (ibuf->channels ? ibuf->channels : 4);
|
||||
const Py_ssize_t itemsize = is_byte ? 1 : Py_ssize_t(sizeof(float));
|
||||
const char *format = is_byte ? "B" : "f";
|
||||
|
||||
PyObject *memview;
|
||||
|
||||
if (self->region == std::nullopt) {
|
||||
Py_ssize_t num_items = Py_ssize_t(ibuf->x) * ibuf->y * channels;
|
||||
|
||||
Py_buffer pybuf;
|
||||
memset(&pybuf, 0, sizeof(pybuf));
|
||||
pybuf.buf = is_byte ? static_cast<void *>(ibuf->byte_buffer.data) :
|
||||
static_cast<void *>(ibuf->float_buffer.data);
|
||||
pybuf.len = num_items * itemsize;
|
||||
pybuf.itemsize = itemsize;
|
||||
pybuf.readonly = !self->writable;
|
||||
pybuf.format = const_cast<char *>(format);
|
||||
pybuf.ndim = 1;
|
||||
pybuf.shape = &num_items;
|
||||
pybuf.strides = &pybuf.itemsize;
|
||||
memview = PyMemoryView_FromBuffer(&pybuf);
|
||||
}
|
||||
else {
|
||||
const rcti ®ion = *self->region;
|
||||
const int xmin = region.xmin;
|
||||
const int ymin = region.ymin;
|
||||
const int xmax = region.xmax;
|
||||
const int ymax = region.ymax;
|
||||
|
||||
const Py_ssize_t row_stride = Py_ssize_t(ibuf->x) * channels * itemsize;
|
||||
const Py_ssize_t offset = (Py_ssize_t(ymin) * ibuf->x + xmin) * channels * itemsize;
|
||||
|
||||
Py_ssize_t shape[2] = {ymax - ymin, (xmax - xmin) * channels};
|
||||
Py_ssize_t strides[2] = {row_stride, itemsize};
|
||||
|
||||
Py_buffer pybuf;
|
||||
memset(&pybuf, 0, sizeof(pybuf));
|
||||
pybuf.buf = is_byte ? static_cast<void *>(ibuf->byte_buffer.data + offset) :
|
||||
static_cast<void *>(reinterpret_cast<char *>(ibuf->float_buffer.data) +
|
||||
offset);
|
||||
pybuf.len = shape[0] * shape[1] * itemsize;
|
||||
pybuf.itemsize = itemsize;
|
||||
pybuf.readonly = !self->writable;
|
||||
pybuf.format = const_cast<char *>(format);
|
||||
pybuf.ndim = 2;
|
||||
pybuf.shape = shape;
|
||||
pybuf.strides = strides;
|
||||
memview = PyMemoryView_FromBuffer(&pybuf);
|
||||
}
|
||||
|
||||
if (UNLIKELY(memview == nullptr)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
self->is_entered = true;
|
||||
self->py_ibuf->buffer_users++;
|
||||
return memview;
|
||||
}
|
||||
|
||||
static PyObject *py_imbuf_buffer_exit(Py_ImBufBuffer *self, PyObject * /*args*/)
|
||||
{
|
||||
if (self->is_entered && self->py_ibuf) {
|
||||
self->is_entered = false;
|
||||
self->py_ibuf->buffer_users--;
|
||||
BLI_assert(self->py_ibuf->buffer_users >= 0);
|
||||
|
||||
if (self->writable) {
|
||||
ImBuf *ibuf = self->py_ibuf->ibuf;
|
||||
if (ibuf != nullptr) {
|
||||
if (self->mode == IB_byte_data) {
|
||||
if (ibuf->float_buffer.data != nullptr) {
|
||||
IMB_float_from_byte(ibuf);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (ibuf->byte_buffer.data != nullptr) {
|
||||
IMB_byte_from_float(ibuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
#ifdef __GNUC__
|
||||
# ifdef __clang__
|
||||
# pragma clang diagnostic push
|
||||
# pragma clang diagnostic ignored "-Wcast-function-type"
|
||||
# else
|
||||
# pragma GCC diagnostic push
|
||||
# pragma GCC diagnostic ignored "-Wcast-function-type"
|
||||
# endif
|
||||
#endif
|
||||
|
||||
static PyMethodDef Py_ImBufBuffer_methods[] = {
|
||||
{"__enter__", reinterpret_cast<PyCFunction>(py_imbuf_buffer_enter), METH_NOARGS, nullptr},
|
||||
{"__exit__", reinterpret_cast<PyCFunction>(py_imbuf_buffer_exit), METH_VARARGS, nullptr},
|
||||
{nullptr, nullptr, 0, nullptr},
|
||||
};
|
||||
|
||||
#ifdef __GNUC__
|
||||
# ifdef __clang__
|
||||
# pragma clang diagnostic pop
|
||||
# else
|
||||
# pragma GCC diagnostic pop
|
||||
# endif
|
||||
#endif
|
||||
|
||||
PyTypeObject Py_ImBufBuffer_Type = {
|
||||
/*ob_base*/ PyVarObject_HEAD_INIT(nullptr, 0)
|
||||
/*tp_name*/ "ImBufBuffer",
|
||||
/*tp_basicsize*/ sizeof(Py_ImBufBuffer),
|
||||
/*tp_itemsize*/ 0,
|
||||
/*tp_dealloc*/ reinterpret_cast<destructor>(py_imbuf_buffer_dealloc),
|
||||
/*tp_vectorcall_offset*/ 0,
|
||||
/*tp_getattr*/ nullptr,
|
||||
/*tp_setattr*/ nullptr,
|
||||
/*tp_as_async*/ nullptr,
|
||||
/*tp_repr*/ reinterpret_cast<reprfunc>(py_imbuf_buffer_repr),
|
||||
/*tp_as_number*/ nullptr,
|
||||
/*tp_as_sequence*/ nullptr,
|
||||
/*tp_as_mapping*/ nullptr,
|
||||
/*tp_hash*/ PyObject_HashNotImplemented,
|
||||
/*tp_call*/ nullptr,
|
||||
/*tp_str*/ nullptr,
|
||||
/*tp_getattro*/ nullptr,
|
||||
/*tp_setattro*/ nullptr,
|
||||
/*tp_as_buffer*/ nullptr,
|
||||
/*tp_flags*/ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,
|
||||
/*tp_doc*/ nullptr,
|
||||
/*tp_traverse*/ reinterpret_cast<traverseproc>(py_imbuf_buffer_traverse),
|
||||
/*tp_clear*/ reinterpret_cast<inquiry>(py_imbuf_buffer_clear),
|
||||
/*tp_richcompare*/ nullptr,
|
||||
/*tp_weaklistoffset*/ 0,
|
||||
/*tp_iter*/ nullptr,
|
||||
/*tp_iternext*/ nullptr,
|
||||
/*tp_methods*/ Py_ImBufBuffer_methods,
|
||||
/*tp_members*/ nullptr,
|
||||
/*tp_getset*/ nullptr,
|
||||
/*tp_base*/ nullptr,
|
||||
/*tp_dict*/ nullptr,
|
||||
/*tp_descr_get*/ nullptr,
|
||||
/*tp_descr_set*/ nullptr,
|
||||
/*tp_dictoffset*/ 0,
|
||||
/*tp_init*/ nullptr,
|
||||
/*tp_alloc*/ nullptr,
|
||||
/*tp_new*/ nullptr,
|
||||
/*tp_free*/ nullptr,
|
||||
/*tp_is_gc*/ nullptr,
|
||||
/*tp_bases*/ nullptr,
|
||||
/*tp_mro*/ nullptr,
|
||||
/*tp_cache*/ nullptr,
|
||||
/*tp_subclasses*/ nullptr,
|
||||
/*tp_weaklist*/ nullptr,
|
||||
/*tp_del*/ nullptr,
|
||||
/*tp_version_tag*/ 0,
|
||||
/*tp_finalize*/ nullptr,
|
||||
/*tp_vectorcall*/ nullptr,
|
||||
};
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Module Functions
|
||||
* \{ */
|
||||
|
|
@ -984,6 +1516,7 @@ PyObject *BPyInit_imbuf_types()
|
|||
}
|
||||
|
||||
PyModule_AddType(submodule, &Py_ImBuf_Type);
|
||||
PyModule_AddType(submodule, &Py_ImBufBuffer_Type);
|
||||
|
||||
return submodule;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
#include "BLI_utildefines.h" /* for bool */
|
||||
|
||||
#include "DNA_vec_types.h" /* for rcti */
|
||||
|
||||
#include "py_capi_utils.hh"
|
||||
|
||||
#include "python_utildefines.hh"
|
||||
|
|
@ -606,6 +608,30 @@ int PyC_ParseOptionalBool(PyObject *o, void *p)
|
|||
return 1;
|
||||
}
|
||||
|
||||
int PyC_ParseRectI(PyObject *o, void *p)
|
||||
{
|
||||
rcti *rect = static_cast<rcti *>(p);
|
||||
if (!PyArg_ParseTuple(o, "(ii)(ii)", &rect->xmin, &rect->ymin, &rect->xmax, &rect->ymax)) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int PyC_ParseOptionalRectI(PyObject *o, void *p)
|
||||
{
|
||||
std::optional<rcti> *value_p = static_cast<std::optional<rcti> *>(p);
|
||||
if (o == Py_None) {
|
||||
value_p->reset();
|
||||
return 1;
|
||||
}
|
||||
rcti rect;
|
||||
if (!PyC_ParseRectI(o, &rect)) {
|
||||
return 0;
|
||||
}
|
||||
*value_p = rect;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int PyC_ParseStringEnum(PyObject *o, void *p)
|
||||
{
|
||||
PyC_StringEnum *e = static_cast<PyC_StringEnum *>(p);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
#include "BLI_span.hh"
|
||||
#include "BLI_sys_types.h"
|
||||
|
||||
#include "DNA_vec_types.h"
|
||||
|
||||
namespace blender {
|
||||
|
||||
/** Useful to print Python objects while debugging. */
|
||||
|
|
@ -355,6 +357,18 @@ struct PyC_TypeOrNone {
|
|||
*/
|
||||
[[nodiscard]] int PyC_ParseOptionalBool(PyObject *o, void *p);
|
||||
|
||||
/**
|
||||
* Use with PyArg_ParseTuple's "O&" formatting.
|
||||
*
|
||||
* Parse `((x1, y1), (x2, y2))` into an `rcti`.
|
||||
*/
|
||||
[[nodiscard]] int PyC_ParseRectI(PyObject *o, void *p);
|
||||
/**
|
||||
* A version of #PyC_ParseRectI that accepts None
|
||||
* (leaving the `std::optional<rcti>` empty).
|
||||
*/
|
||||
[[nodiscard]] int PyC_ParseOptionalRectI(PyObject *o, void *p);
|
||||
|
||||
/**
|
||||
* Cast a pointer of a PyObject-derived type to `PyObject *`.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@ from imbuf.types import ImBuf
|
|||
|
||||
DEFAULT_SIZE = 32, 32
|
||||
|
||||
# Test color in linear float, sRGB byte, and the expected conversions between them.
|
||||
COLOR_FLOAT = (1.0, 0.5, 0.25, 1.0)
|
||||
COLOR_BYTE = (255, 128, 196, 255)
|
||||
# sRGB byte -> linear float.
|
||||
COLOR_BYTE_AS_FLOAT = (1.0, 0.2159, 0.5520, 1.0)
|
||||
# Linear float -> sRGB byte.
|
||||
COLOR_FLOAT_AS_BYTE = (255, 188, 137, 255)
|
||||
|
||||
# NOTE: an invalid file format & missing files is aborting on the build-bot.
|
||||
# Disable as it makes tests fail, although we could consider demoting this to a warning
|
||||
# since it prevents valid code-paths from being tested.
|
||||
|
|
@ -407,6 +415,285 @@ class TestImBufPlanes(unittest.TestCase):
|
|||
imbuf.new((2, 2), planes=p)
|
||||
|
||||
|
||||
class TestImBufPixelsBytes(unittest.TestCase):
|
||||
|
||||
def test_read_only_by_default(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
self.assertEqual(buf.readonly, True)
|
||||
with self.assertRaises(TypeError):
|
||||
buf[0] = 0xFF
|
||||
ibuf.free()
|
||||
|
||||
def test_write_mode(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
with ibuf.with_buffer('BYTE', write=True) as buf:
|
||||
self.assertEqual(buf.readonly, False)
|
||||
buf[0] = 42
|
||||
self.assertEqual(buf[0], 42)
|
||||
ibuf.free()
|
||||
|
||||
def test_buffer_length(self):
|
||||
w, h = 8, 6
|
||||
ibuf = imbuf.new((w, h))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
self.assertEqual(len(buf), w * h * 4)
|
||||
ibuf.free()
|
||||
|
||||
def test_freed_image(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.free()
|
||||
with self.assertRaises(ReferenceError):
|
||||
ibuf.with_buffer('BYTE')
|
||||
|
||||
def test_exports_block_resize(self):
|
||||
ibuf = imbuf.new((4, 4))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
with self.assertRaises(BufferError):
|
||||
ibuf.resize((2, 2))
|
||||
# After context exit, resize works.
|
||||
ibuf.resize((2, 2))
|
||||
self.assertEqual(ibuf.size, (2, 2))
|
||||
ibuf.free()
|
||||
|
||||
def test_exports_block_crop(self):
|
||||
ibuf = imbuf.new((4, 4))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
with self.assertRaises(BufferError):
|
||||
ibuf.crop((0, 0), (1, 1))
|
||||
ibuf.free()
|
||||
|
||||
def test_exports_block_free(self):
|
||||
ibuf = imbuf.new((4, 4))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
with self.assertRaises(BufferError):
|
||||
ibuf.free()
|
||||
ibuf.free()
|
||||
|
||||
def test_double_enter_blocked(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ctx = ibuf.with_buffer('BYTE')
|
||||
ctx.__enter__()
|
||||
with self.assertRaises(RuntimeError):
|
||||
ctx.__enter__()
|
||||
ctx.__exit__(None, None, None)
|
||||
ibuf.free()
|
||||
|
||||
def test_reentry_after_exit(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ctx = ibuf.with_buffer('BYTE', write=True)
|
||||
buf1 = ctx.__enter__()
|
||||
buf1[0] = 10
|
||||
ctx.__exit__(None, None, None)
|
||||
buf2 = ctx.__enter__()
|
||||
self.assertEqual(buf2[0], 10)
|
||||
ctx.__exit__(None, None, None)
|
||||
ibuf.free()
|
||||
|
||||
|
||||
class TestImBufPixelsRegion(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.ibuf = imbuf.new((8, 6))
|
||||
with self.ibuf.with_buffer('BYTE', write=True) as buf:
|
||||
for i in range(8 * 6):
|
||||
buf[i * 4] = i % 256
|
||||
|
||||
def tearDown(self):
|
||||
self.ibuf.free()
|
||||
|
||||
def test_full_image_1d(self):
|
||||
with self.ibuf.with_buffer('BYTE') as buf:
|
||||
self.assertEqual(buf.ndim, 1)
|
||||
self.assertEqual(len(buf), 8 * 6 * 4)
|
||||
|
||||
def test_region_none_is_1d(self):
|
||||
with self.ibuf.with_buffer('BYTE', region=None) as buf:
|
||||
self.assertEqual(buf.ndim, 1)
|
||||
|
||||
def test_sub_region_2d(self):
|
||||
with self.ibuf.with_buffer('BYTE', region=((2, 1), (6, 4))) as buf:
|
||||
self.assertEqual(buf.ndim, 2)
|
||||
self.assertEqual(buf.shape, (3, 16))
|
||||
|
||||
def test_region_pixel_values(self):
|
||||
with self.ibuf.with_buffer('BYTE', region=((2, 1), (6, 4))) as buf:
|
||||
# Pixel (2, 1) has index 1*8+2 = 10.
|
||||
self.assertEqual(buf[0, 0], 10)
|
||||
# Pixel (2, 2) has index 2*8+2 = 18.
|
||||
self.assertEqual(buf[1, 0], 18)
|
||||
|
||||
def test_full_image_region_collapses_to_1d(self):
|
||||
with self.ibuf.with_buffer('BYTE', region=((0, 0), (8, 6))) as buf:
|
||||
self.assertEqual(buf.ndim, 1)
|
||||
|
||||
def test_oversized_region_collapses_to_1d(self):
|
||||
with self.ibuf.with_buffer('BYTE', region=((-5, -5), (100, 100))) as buf:
|
||||
self.assertEqual(buf.ndim, 1)
|
||||
|
||||
def test_zero_area_region(self):
|
||||
with self.ibuf.with_buffer('BYTE', region=((100, 100), (200, 200))) as buf:
|
||||
self.assertEqual(buf.ndim, 2)
|
||||
self.assertEqual(buf.shape[0] * buf.shape[1], 0)
|
||||
|
||||
with self.ibuf.with_buffer('BYTE', region=((5, 5), (2, 2))) as buf:
|
||||
self.assertEqual(buf.shape[0] * buf.shape[1], 0)
|
||||
|
||||
with self.ibuf.with_buffer('BYTE', region=((3, 3), (3, 3))) as buf:
|
||||
self.assertEqual(buf.shape[0] * buf.shape[1], 0)
|
||||
|
||||
def test_region_write_does_not_corrupt_neighbors(self):
|
||||
with self.ibuf.with_buffer('BYTE', write=True, region=((1, 1), (3, 3))) as buf:
|
||||
for r in range(buf.shape[0]):
|
||||
for c in range(buf.shape[1]):
|
||||
buf[r, c] = 0xFF
|
||||
with self.ibuf.with_buffer('BYTE') as buf:
|
||||
self.assertEqual(buf[0], 0)
|
||||
inside = (1 * 8 + 1) * 4
|
||||
self.assertEqual(buf[inside], 0xFF)
|
||||
|
||||
|
||||
class TestImBufPixelsFloat(unittest.TestCase):
|
||||
|
||||
def test_no_float_buffer_error(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
with self.assertRaises(RuntimeError):
|
||||
ibuf.with_buffer('FLOAT')
|
||||
ibuf.free()
|
||||
|
||||
def test_ensure_and_access(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
with ibuf.with_buffer('FLOAT') as buf:
|
||||
self.assertEqual(buf.format, "f")
|
||||
ibuf.free()
|
||||
|
||||
def test_float_write(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
with ibuf.with_buffer('FLOAT', write=True) as buf:
|
||||
buf[0] = 0.5
|
||||
self.assertAlmostEqual(buf[0], 0.5, places=5)
|
||||
ibuf.free()
|
||||
|
||||
|
||||
class TestImBufEnsureBuffers(unittest.TestCase):
|
||||
|
||||
def test_ensure_float_from_byte(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
with ibuf.with_buffer('BYTE', write=True) as buf:
|
||||
buf[0:4] = bytes(COLOR_BYTE)
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
with ibuf.with_buffer('FLOAT') as buf:
|
||||
for i, expected in enumerate(COLOR_BYTE_AS_FLOAT):
|
||||
self.assertAlmostEqual(buf[i], expected, places=3)
|
||||
ibuf.free()
|
||||
|
||||
def test_ensure_byte_from_float(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
with ibuf.with_buffer('FLOAT', write=True) as buf:
|
||||
for i, v in enumerate(COLOR_FLOAT):
|
||||
buf[i] = v
|
||||
ibuf.clear_buffer('BYTE')
|
||||
self.assertFalse(ibuf.has_buffer('BYTE'))
|
||||
ibuf.ensure_buffer('BYTE')
|
||||
self.assertTrue(ibuf.has_buffer('BYTE'))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
for i, expected in enumerate(COLOR_FLOAT_AS_BYTE):
|
||||
self.assertEqual(buf[i], expected)
|
||||
ibuf.free()
|
||||
|
||||
def test_ensure_noop_when_present(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('BYTE')
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
ibuf.free()
|
||||
|
||||
def test_ensure_blocked_while_exported(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
with self.assertRaises(BufferError):
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
ibuf.free()
|
||||
|
||||
|
||||
class TestImBufHasAndClearBuffer(unittest.TestCase):
|
||||
|
||||
def test_has_buffer_byte(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
self.assertTrue(ibuf.has_buffer('BYTE'))
|
||||
self.assertFalse(ibuf.has_buffer('FLOAT'))
|
||||
ibuf.free()
|
||||
|
||||
def test_clear_buffer_byte(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
self.assertTrue(ibuf.has_buffer('BYTE'))
|
||||
ibuf.clear_buffer('BYTE')
|
||||
self.assertFalse(ibuf.has_buffer('BYTE'))
|
||||
ibuf.free()
|
||||
|
||||
def test_clear_buffer_float(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
self.assertTrue(ibuf.has_buffer('FLOAT'))
|
||||
ibuf.clear_buffer('FLOAT')
|
||||
self.assertFalse(ibuf.has_buffer('FLOAT'))
|
||||
ibuf.free()
|
||||
|
||||
def test_clear_buffer_blocked_while_exported(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
with self.assertRaises(BufferError):
|
||||
ibuf.clear_buffer('BYTE')
|
||||
ibuf.free()
|
||||
|
||||
def test_clear_already_absent(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.clear_buffer('FLOAT')
|
||||
ibuf.free()
|
||||
|
||||
|
||||
class TestImBufExitSync(unittest.TestCase):
|
||||
|
||||
def test_byte_write_syncs_to_float(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
with ibuf.with_buffer('BYTE', write=True) as buf:
|
||||
for i, v in enumerate(COLOR_BYTE):
|
||||
buf[i] = v
|
||||
with ibuf.with_buffer('FLOAT') as buf:
|
||||
for i, expected in enumerate(COLOR_BYTE_AS_FLOAT):
|
||||
self.assertAlmostEqual(buf[i], expected, places=3)
|
||||
ibuf.free()
|
||||
|
||||
def test_float_write_syncs_to_byte(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
with ibuf.with_buffer('FLOAT', write=True) as buf:
|
||||
for i, v in enumerate(COLOR_FLOAT):
|
||||
buf[i] = v
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
for i, expected in enumerate(COLOR_FLOAT_AS_BYTE):
|
||||
self.assertEqual(buf[i], expected)
|
||||
ibuf.free()
|
||||
|
||||
def test_read_only_does_not_sync(self):
|
||||
ibuf = imbuf.new((2, 2))
|
||||
ibuf.ensure_buffer('FLOAT')
|
||||
with ibuf.with_buffer('FLOAT', write=True) as buf:
|
||||
for i, v in enumerate(COLOR_FLOAT):
|
||||
buf[i] = v
|
||||
# Read-only byte access should NOT overwrite the float buffer.
|
||||
with ibuf.with_buffer('BYTE') as buf:
|
||||
_ = buf[0]
|
||||
with ibuf.with_buffer('FLOAT') as buf:
|
||||
for i, expected in enumerate(COLOR_FLOAT):
|
||||
self.assertAlmostEqual(buf[i], expected, places=3)
|
||||
ibuf.free()
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue