File: pixelaccess.py

package info (click to toggle)
pysdl2 0.9.17%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 3,328 kB
  • sloc: python: 24,685; makefile: 36; sh: 8
file content (321 lines) | stat: -rw-r--r-- 12,541 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
import ctypes
from .compat import UnsupportedError, experimental
from .array import MemoryView
from ..surface import SDL_MUSTLOCK, SDL_LockSurface, SDL_UnlockSurface, \
    SDL_Surface
from ..stdinc import Uint8
from .draw import prepare_color
from .sprite import SoftwareSprite
from .surface import _get_target_surface

try:
    import numpy
    _HASNUMPY = True
except ImportError:
    _HASNUMPY = False


__all__ = [
    "PixelView", "SurfaceArray", "pixels2d", "pixels3d", "surface_to_ndarray"
]

class PixelView(MemoryView):
    """A 2D memory view for reading and writing SDL surface pixels.

    This class uses a ``view[y][x]`` layout, with the y-axis as the first
    dimension and the x-axis as the second. ``PixelView`` objects currently do
    not support array slicing, but support negative indexing as of
    PySDL2 0.9.10.

    If the source surface is RLE-accelerated, it will be locked automatically
    when the view is created and you will need to re-lock the surface using
    :func:`SDL_UnlockSurface` once you are done with the view.

    .. warning::
       The source surface should not be freed or deleted until the view is no
       longer needed. Accessing the view for a freed surface will likely cause
       Python to hard-crash.

    .. note:: 
       This class is implemented on top of the :class:`~sdl2.ext.MemoryView`
       class. As such, it makes heavy use of recursion to access rows and
       will generally be much slower than the :mod:`numpy`-based
       :func:`~sdl2.ext.pixels2d` and :func:`~sdl2.ext.pixels3d` functions.

    Args:
        source (:obj:`~sdl2.SDL_Surface`, :obj:`~sdl2.ext.SoftwareSprite`): The
            SDL surface to access with the view.

    """
    def __init__(self, source):
        if isinstance(source, SoftwareSprite):
            self._surface = source.surface
            # keep a reference, so the Sprite's not GC'd
            self._sprite = source
        elif isinstance(source, SDL_Surface):
            self._surface = source
        elif "SDL_Surface" in str(type(source)):
            self._surface = source.contents
        else:
            raise TypeError("source must be a Sprite or SDL_Surface")

        itemsize = self._surface.format.contents.BytesPerPixel
        if itemsize == 3:
            e = "Cannot open a 3 bytes-per-pixel surface using a PixelView."
            raise RuntimeError(e)

        if SDL_MUSTLOCK(self._surface):
            SDL_LockSurface(self._surface)

        pxbuf = ctypes.cast(self._surface.pixels, ctypes.POINTER(Uint8))
        strides = (self._surface.h, self._surface.w)
        srcsize = self._surface.h * self._surface.pitch
        super(PixelView, self).__init__(pxbuf, itemsize, strides,
                                        getfunc=self._getitem,
                                        setfunc=self._setitem,
                                        srcsize=srcsize)

    def _getitem(self, start, end):
        if self.itemsize == 1:
            # byte-wise access
            return self.source[start:end]
        # move the pointer to the correct location
        src = ctypes.byref(self.source.contents, start)
        casttype = ctypes.c_ubyte
        if self.itemsize == 2:
            casttype = ctypes.c_ushort
        elif self.itemsize == 4:
            casttype = ctypes.c_uint
        return ctypes.cast(src, ctypes.POINTER(casttype)).contents.value

    def _setitem(self, start, end, value):
        target = None
        if self.itemsize == 1:
            target = ctypes.cast(self.source, ctypes.POINTER(ctypes.c_ubyte))
        elif self.itemsize == 2:
            target = ctypes.cast(self.source, ctypes.POINTER(ctypes.c_ushort))
        elif self.itemsize == 4:
            target = ctypes.cast(self.source, ctypes.POINTER(ctypes.c_uint))
        value = prepare_color(value, self._surface)
        target[start // self.itemsize] = value


def _ndarray_prep(source, funcname, ndim):
    # Internal function for preparing SDL_Surfaces for casting to ndarrays
    if not _HASNUMPY:
        err = "'{0}' requires Numpy, which could not be found."
        raise UnsupportedError(err.format(funcname))

    # Get SDL surface and extract required attributes
    psurface = _get_target_surface(source, argname="source")
    sz = psurface.h * psurface.pitch
    bpp = psurface.format.contents.BytesPerPixel
    if bpp < 1 or bpp > 4:
        err = "The bpp of the source surface must be between 1 and 4, inclusive"
        raise ValueError(err + " (got {0}).".format(bpp))
    elif bpp == 3 and ndim == 2:
        err = "Surfaces with 3 bytes-per-pixel cannot be cast as 2D arrays."
        raise RuntimeError(err)

    # Handle 2D and 3D arrays differently where needed
    if ndim == 2:
        dtypes = {
            1: numpy.uint8,
            2: numpy.uint16,
            4: numpy.uint32
        }
        strides = (psurface.pitch, bpp)
        shape = psurface.h, psurface.w
        dtype = dtypes[bpp]
    else:
        strides = (psurface.pitch, bpp, 1)
        shape = psurface.h, psurface.w, bpp
        dtype = numpy.uint8

    return (psurface, sz, shape, dtype, strides)


def pixels2d(source, transpose=True):
    """Creates a 2D Numpy array view for a given SDL surface.

    This function casts the surface pixels to a 2D Numpy array view, providing
    read and write access to the underlying surface. If the source surface is
    RLE-accelerated, it will be locked automatically when the view is created
    and you will need to re-lock the surface using :func:`SDL_UnlockSurface`
    once you are done with the array.

    By default, the array is returned in ``arr[x][y]`` format with the x-axis
    as the first dimension, contrary to PIL and PyOpenGL convention. To obtain 
    an ``arr[y][x]`` array, set the ``transpose`` argument to ``False``.

    .. warning::
       The source surface should not be freed or deleted until the array is no
       longer needed. Accessing the array for a freed surface will likely cause
       Python to hard-crash.

    .. note::
       This function requires Numpy to be installed in the current Python
       environment.

    Args:
        source (:obj:`~sdl2.SDL_Surface`, :obj:`~sdl2.ext.SoftwareSprite`): The
            SDL surface to cast to a numpy array.
        transpose (bool, optional): Whether the output array should be
            transposed to have ``arr[x][y]`` axes instead of ``arr[y][x]`` axes.
            Defaults to ``True``.

    Returns:
        :obj:`numpy.ndarray`: A 2-dimensional Numpy array containing the integer
        color values for each pixel in the surface.

    Raises:
        RuntimeError: If Numpy could not be imported.
   
    """
    sf, sz, shape, dtype, strides = _ndarray_prep(source, "pixels2d", ndim=2)
    if SDL_MUSTLOCK(sf):
        SDL_LockSurface(sf)

    pxbuf = ctypes.cast(sf.pixels, ctypes.POINTER(ctypes.c_ubyte * sz))
    arr = SurfaceArray(
        shape, dtype, pxbuf.contents, 0, strides, "C", source, sf
    )
    return arr.transpose() if transpose else arr


def pixels3d(source, transpose=True):
    """Creates a 3D Numpy array view for a given SDL surface.

    This function casts the surface pixels to a 3D Numpy array view, providing
    read and write access to the underlying surface. If the source surface is
    RLE-accelerated, it will be locked automatically when the view is created
    and you will need to re-lock the surface using :func:`SDL_UnlockSurface`
    once you are done with the array.

    By default, the array is returned in ``arr[x][y]`` format with the x-axis
    as the first dimension, contrary to PIL and PyOpenGL convention. To obtain 
    an ``arr[y][x]`` array, set the ``transpose`` argument to ``False``.

    When creating a 3D array view, the order of the RGBA values for each pixel
    may be reversed for some common surface pixel formats (e.g. 'BGRA' for a
    ``SDL_PIXELFORMAT_ARGB8888`` surface). To correct this, you can call
    ``numpy.flip(arr, axis=2)`` to return a view of the array with the expected
    channel order.

    .. warning::
       The source surface should not be freed or deleted until the array is no
       longer needed. Accessing the array for a freed surface will likely cause
       Python to hard-crash.

    .. note::
       This function requires Numpy to be installed in the current Python
       environment.

    Args:
        source (:obj:`~sdl2.SDL_Surface`, :obj:`~sdl2.ext.SoftwareSprite`): The
            SDL surface to cast to a numpy array.
        transpose (bool, optional): Whether the output array should be
            transposed to have ``arr[x][y]`` axes instead of ``arr[y][x]`` axes.
            Defaults to ``True``.

    Returns:
        :obj:`numpy.ndarray`: A 3-dimensional Numpy array containing the values
        of each byte for each pixel in the surface.

    Raises:
        RuntimeError: If Numpy could not be imported.
   
    """
    sf, sz, shape, dtype, strides = _ndarray_prep(source, "pixels3d", ndim=3)
    if SDL_MUSTLOCK(sf):
        SDL_LockSurface(sf)

    pxbuf = ctypes.cast(sf.pixels, ctypes.POINTER(ctypes.c_ubyte * sz))
    arr = SurfaceArray(
        shape, dtype, pxbuf.contents, 0, strides, "C", source, sf
    )
    return arr.transpose(1, 0, 2) if transpose else arr


def surface_to_ndarray(source, ndim=3):
    """Returns a copy of an SDL surface as a Numpy array.
    
    The main difference between this function and :func:`~sdl2.ext.pixels2d` or
    :func:`~sdl2.ext.pixels3d` is that it returns a copy of the surface instead
    of a view, meaning that modifying the returned array will not affect the
    original surface (or vice-versa). This function is also slightly safer,
    as it does not assume that the source surface has been kept in memory.

    When creating a 3D array copy, the order of the RGBA values for each pixel
    may be reversed for some common surface pixel formats (e.g. 'BGRA' for a
    ``SDL_PIXELFORMAT_ARGB8888`` surface). To correct this, you can call
    ``numpy.flip(arr, axis=2)`` to return a view of the array with the expected
    channel order.

    .. note::
       Unlike :func:`~sdl2.ext.pixels2d` or :func:`~sdl2.ext.pixels3d`, this
       function always returns arrays with the y-axis as the first dimension
       (e.g. ``arr[y][x]``).

    .. note::
       This function requires Numpy to be installed in the current Python
       environment.

    Args:
        source (:obj:`~sdl2.SDL_Surface`, :obj:`~sdl2.ext.SoftwareSprite`): The
            SDL surface to convert to a numpy array.
        ndim (int, optional): The number of dimensions for the returned array,
            must be either 2 (for a 2D array) or 3 (for a 3D array). Defaults
            to 3.

    Returns:
        :obj:`numpy.ndarray`: A Numpy array containing a copy of the pixel data
        for the given surface.

    Raises:
        RuntimeError: If Numpy could not be imported.

    """
    if ndim not in [2, 3]:
        err = "Can only convert surfaces to 2D or 3D arrays (got {0})."
        raise ValueError(err.format(ndim))
    funcname = "surface_to_array"
    sf, sz, shape, dtype, strides = _ndarray_prep(source, funcname, ndim)
    was_unlocked = sf.locked == 0
    if SDL_MUSTLOCK(sf):
        SDL_LockSurface(sf)

    pxbuf = ctypes.cast(sf.pixels, ctypes.POINTER(ctypes.c_ubyte * sz))
    tmp = numpy.ndarray(shape, dtype, pxbuf.contents, strides=strides)
    if was_unlocked and SDL_MUSTLOCK(sf):
        SDL_UnlockSurface(sf)

    return numpy.copy(tmp)


class SurfaceArray(numpy.ndarray if _HASNUMPY else object):
    """A Numpy array that keeps a reference to its parent SDL surface.

    This class is used to keep track of the original source object for
    :func:`~sdl2.ext.pixels2d` or :func:`~sdl2.ext.pixels3d` to prevent it from
    being automatically freed during garbage collection. It should never be used
    for any other purpose.
    
    """
    def __new__(cls, shape, dtype=float, buffer_=None, offset=0,
                strides=None, order=None, source=None, surface=None):
        if _HASNUMPY:
            sfarray = numpy.ndarray.__new__(
                cls, shape, dtype, buffer_, offset, strides, order
            )
            sfarray._source = source
            sfarray._surface = surface
            return sfarray
        else:
            return None

    def __array_finalize__(self, sfarray):
        if sfarray is None:
            return
        self._source = getattr(sfarray, '_source', None)
        self._surface = getattr(sfarray, '_surface', None)