pyglet 2.1.12__py3-none-any.whl → 3.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyglet/__init__.py +67 -61
- pyglet/__init__.pyi +15 -8
- pyglet/app/__init__.py +22 -13
- pyglet/app/async_app.py +212 -0
- pyglet/app/base.py +2 -1
- pyglet/app/{xlib.py → linux.py} +3 -3
- pyglet/config/__init__.py +101 -0
- pyglet/config/gl/__init__.py +30 -0
- pyglet/config/gl/egl.py +120 -0
- pyglet/config/gl/macos.py +262 -0
- pyglet/config/gl/windows.py +267 -0
- pyglet/config/gl/x11.py +142 -0
- pyglet/customtypes.py +43 -2
- pyglet/display/__init__.py +8 -6
- pyglet/display/base.py +3 -63
- pyglet/display/cocoa.py +12 -17
- pyglet/display/emscripten.py +39 -0
- pyglet/display/headless.py +23 -30
- pyglet/display/wayland.py +157 -0
- pyglet/display/win32.py +4 -17
- pyglet/display/xlib.py +19 -27
- pyglet/display/xlib_vidmoderestore.py +2 -2
- pyglet/enums.py +183 -0
- pyglet/event.py +0 -1
- pyglet/experimental/geoshader_sprite.py +15 -13
- pyglet/experimental/hidraw.py +6 -15
- pyglet/experimental/multitexture_sprite.py +31 -19
- pyglet/experimental/particles.py +13 -35
- pyglet/font/__init__.py +251 -85
- pyglet/font/base.py +116 -61
- pyglet/font/dwrite/__init__.py +349 -204
- pyglet/font/dwrite/dwrite_lib.py +27 -5
- pyglet/font/fontconfig.py +14 -6
- pyglet/font/freetype.py +138 -87
- pyglet/font/freetype_lib.py +19 -0
- pyglet/font/group.py +179 -0
- pyglet/font/harfbuzz/__init__.py +3 -3
- pyglet/font/pyodide_js.py +310 -0
- pyglet/font/quartz.py +319 -126
- pyglet/font/ttf.py +45 -3
- pyglet/font/user.py +14 -19
- pyglet/font/win32.py +45 -21
- pyglet/graphics/__init__.py +8 -787
- pyglet/graphics/allocation.py +115 -1
- pyglet/graphics/api/__init__.py +77 -0
- pyglet/graphics/api/base.py +299 -0
- pyglet/graphics/api/gl/__init__.py +58 -0
- pyglet/graphics/api/gl/base.py +24 -0
- pyglet/graphics/{vertexbuffer.py → api/gl/buffer.py} +104 -159
- pyglet/graphics/api/gl/cocoa/context.py +76 -0
- pyglet/graphics/api/gl/context.py +391 -0
- pyglet/graphics/api/gl/default_shaders.py +0 -0
- pyglet/graphics/api/gl/draw.py +627 -0
- pyglet/graphics/api/gl/egl/__init__.py +0 -0
- pyglet/graphics/api/gl/egl/context.py +92 -0
- pyglet/graphics/api/gl/enums.py +76 -0
- pyglet/graphics/api/gl/framebuffer.py +315 -0
- pyglet/graphics/api/gl/gl.py +5463 -0
- pyglet/graphics/api/gl/gl_info.py +188 -0
- pyglet/graphics/api/gl/global_opengl.py +226 -0
- pyglet/{gl → graphics/api/gl}/lib.py +34 -18
- pyglet/graphics/api/gl/shader.py +1476 -0
- pyglet/graphics/api/gl/shapes.py +55 -0
- pyglet/graphics/api/gl/sprite.py +102 -0
- pyglet/graphics/api/gl/state.py +219 -0
- pyglet/graphics/api/gl/text.py +190 -0
- pyglet/graphics/api/gl/texture.py +1526 -0
- pyglet/graphics/{vertexarray.py → api/gl/vertexarray.py} +11 -13
- pyglet/graphics/api/gl/vertexdomain.py +751 -0
- pyglet/graphics/api/gl/win32/__init__.py +0 -0
- pyglet/graphics/api/gl/win32/context.py +108 -0
- pyglet/graphics/api/gl/win32/wgl_info.py +24 -0
- pyglet/graphics/api/gl/xlib/__init__.py +0 -0
- pyglet/graphics/api/gl/xlib/context.py +174 -0
- pyglet/{gl → graphics/api/gl/xlib}/glx_info.py +26 -31
- pyglet/graphics/api/gl1/__init__.py +0 -0
- pyglet/{gl → graphics/api/gl1}/gl_compat.py +3 -2
- pyglet/graphics/api/gl2/__init__.py +0 -0
- pyglet/graphics/api/gl2/buffer.py +320 -0
- pyglet/graphics/api/gl2/draw.py +600 -0
- pyglet/graphics/api/gl2/global_opengl.py +122 -0
- pyglet/graphics/api/gl2/shader.py +200 -0
- pyglet/graphics/api/gl2/shapes.py +51 -0
- pyglet/graphics/api/gl2/sprite.py +79 -0
- pyglet/graphics/api/gl2/text.py +175 -0
- pyglet/graphics/api/gl2/vertexdomain.py +364 -0
- pyglet/graphics/api/webgl/__init__.py +233 -0
- pyglet/graphics/api/webgl/buffer.py +302 -0
- pyglet/graphics/api/webgl/context.py +234 -0
- pyglet/graphics/api/webgl/draw.py +590 -0
- pyglet/graphics/api/webgl/enums.py +76 -0
- pyglet/graphics/api/webgl/framebuffer.py +360 -0
- pyglet/graphics/api/webgl/gl.py +1537 -0
- pyglet/graphics/api/webgl/gl_info.py +130 -0
- pyglet/graphics/api/webgl/shader.py +1346 -0
- pyglet/graphics/api/webgl/shapes.py +92 -0
- pyglet/graphics/api/webgl/sprite.py +102 -0
- pyglet/graphics/api/webgl/state.py +227 -0
- pyglet/graphics/api/webgl/text.py +187 -0
- pyglet/graphics/api/webgl/texture.py +1227 -0
- pyglet/graphics/api/webgl/vertexarray.py +54 -0
- pyglet/graphics/api/webgl/vertexdomain.py +616 -0
- pyglet/graphics/api/webgl/webgl_js.pyi +307 -0
- pyglet/{image → graphics}/atlas.py +33 -32
- pyglet/graphics/base.py +10 -0
- pyglet/graphics/buffer.py +245 -0
- pyglet/graphics/draw.py +578 -0
- pyglet/graphics/framebuffer.py +26 -0
- pyglet/graphics/instance.py +178 -69
- pyglet/graphics/shader.py +267 -1553
- pyglet/graphics/state.py +83 -0
- pyglet/graphics/texture.py +703 -0
- pyglet/graphics/vertexdomain.py +695 -538
- pyglet/gui/ninepatch.py +10 -10
- pyglet/gui/widgets.py +120 -10
- pyglet/image/__init__.py +20 -1973
- pyglet/image/animation.py +12 -12
- pyglet/image/base.py +730 -0
- pyglet/image/codecs/__init__.py +9 -0
- pyglet/image/codecs/bmp.py +53 -30
- pyglet/image/codecs/dds.py +53 -31
- pyglet/image/codecs/gdiplus.py +38 -14
- pyglet/image/codecs/gdkpixbuf2.py +0 -2
- pyglet/image/codecs/js_image.py +99 -0
- pyglet/image/codecs/ktx2.py +161 -0
- pyglet/image/codecs/pil.py +1 -1
- pyglet/image/codecs/png.py +1 -1
- pyglet/image/codecs/wic.py +11 -2
- pyglet/info.py +26 -24
- pyglet/input/__init__.py +8 -0
- pyglet/input/base.py +163 -105
- pyglet/input/controller.py +13 -19
- pyglet/input/controller_db.py +39 -24
- pyglet/input/emscripten/__init__.py +18 -0
- pyglet/input/emscripten/gamepad_js.py +397 -0
- pyglet/input/linux/__init__.py +11 -5
- pyglet/input/linux/evdev.py +10 -11
- pyglet/input/linux/x11_xinput.py +2 -2
- pyglet/input/linux/x11_xinput_tablet.py +1 -1
- pyglet/input/macos/__init__.py +7 -2
- pyglet/input/macos/darwin_gc.py +559 -0
- pyglet/input/win32/__init__.py +1 -1
- pyglet/input/win32/directinput.py +34 -29
- pyglet/input/win32/xinput.py +11 -61
- pyglet/lib.py +3 -3
- pyglet/libs/__init__.py +1 -1
- pyglet/{gl → libs/darwin}/agl.py +1 -1
- pyglet/libs/darwin/cocoapy/__init__.py +2 -2
- pyglet/libs/darwin/cocoapy/cocoahelpers.py +181 -0
- pyglet/libs/darwin/cocoapy/cocoalibs.py +31 -0
- pyglet/libs/darwin/cocoapy/cocoatypes.py +27 -0
- pyglet/libs/darwin/cocoapy/runtime.py +81 -45
- pyglet/libs/darwin/coreaudio.py +4 -4
- pyglet/{gl → libs/darwin}/lib_agl.py +9 -8
- pyglet/libs/darwin/quartzkey.py +1 -3
- pyglet/libs/egl/__init__.py +2 -0
- pyglet/libs/egl/egl_lib.py +576 -0
- pyglet/libs/egl/eglext.py +51 -5
- pyglet/libs/linux/__init__.py +0 -0
- pyglet/libs/linux/egl/__init__.py +0 -0
- pyglet/libs/linux/egl/eglext.py +22 -0
- pyglet/libs/linux/glx/__init__.py +0 -0
- pyglet/{gl → libs/linux/glx}/glx.py +13 -14
- pyglet/{gl → libs/linux/glx}/glxext_arb.py +408 -192
- pyglet/{gl → libs/linux/glx}/glxext_mesa.py +1 -1
- pyglet/{gl → libs/linux/glx}/glxext_nv.py +345 -164
- pyglet/{gl → libs/linux/glx}/lib_glx.py +3 -2
- pyglet/libs/linux/wayland/__init__.py +0 -0
- pyglet/libs/linux/wayland/client.py +1068 -0
- pyglet/libs/linux/wayland/lib_wayland.py +207 -0
- pyglet/libs/linux/wayland/wayland_egl.py +38 -0
- pyglet/libs/{wayland → linux/wayland}/xkbcommon.py +26 -0
- pyglet/libs/{x11 → linux/x11}/xf86vmode.py +4 -4
- pyglet/libs/{x11 → linux/x11}/xinerama.py +2 -2
- pyglet/libs/{x11 → linux/x11}/xinput.py +10 -10
- pyglet/libs/linux/x11/xrandr.py +0 -0
- pyglet/libs/{x11 → linux/x11}/xrender.py +1 -1
- pyglet/libs/shared/__init__.py +0 -0
- pyglet/libs/shared/spirv/__init__.py +0 -0
- pyglet/libs/shared/spirv/lib_shaderc.py +85 -0
- pyglet/libs/shared/spirv/lib_spirv_cross.py +126 -0
- pyglet/libs/win32/__init__.py +27 -5
- pyglet/libs/win32/constants.py +59 -48
- pyglet/libs/win32/context_managers.py +20 -3
- pyglet/libs/win32/dinput.py +105 -88
- pyglet/{gl → libs/win32}/lib_wgl.py +52 -26
- pyglet/libs/win32/types.py +58 -23
- pyglet/{gl → libs/win32}/wgl.py +32 -25
- pyglet/{gl → libs/win32}/wglext_arb.py +364 -2
- pyglet/media/__init__.py +9 -10
- pyglet/media/codecs/__init__.py +12 -1
- pyglet/media/codecs/base.py +99 -96
- pyglet/media/codecs/ffmpeg.py +2 -2
- pyglet/media/codecs/ffmpeg_lib/libavformat.py +3 -8
- pyglet/media/codecs/webaudio_pyodide.py +111 -0
- pyglet/media/drivers/__init__.py +9 -4
- pyglet/media/drivers/base.py +4 -4
- pyglet/media/drivers/openal/__init__.py +1 -1
- pyglet/media/drivers/openal/adaptation.py +3 -3
- pyglet/media/drivers/pulse/__init__.py +1 -1
- pyglet/media/drivers/pulse/adaptation.py +3 -3
- pyglet/media/drivers/pyodide_js/__init__.py +8 -0
- pyglet/media/drivers/pyodide_js/adaptation.py +288 -0
- pyglet/media/drivers/xaudio2/adaptation.py +3 -3
- pyglet/media/player.py +276 -193
- pyglet/media/player_worker_thread.py +1 -1
- pyglet/model/__init__.py +39 -29
- pyglet/model/codecs/base.py +4 -4
- pyglet/model/codecs/gltf.py +3 -3
- pyglet/model/codecs/obj.py +71 -43
- pyglet/resource.py +129 -78
- pyglet/shapes.py +147 -177
- pyglet/sprite.py +47 -164
- pyglet/text/__init__.py +44 -54
- pyglet/text/caret.py +12 -7
- pyglet/text/document.py +19 -17
- pyglet/text/formats/html.py +2 -2
- pyglet/text/formats/structured.py +10 -40
- pyglet/text/layout/__init__.py +20 -13
- pyglet/text/layout/base.py +176 -287
- pyglet/text/layout/incremental.py +9 -10
- pyglet/text/layout/scrolling.py +7 -95
- pyglet/window/__init__.py +183 -172
- pyglet/window/cocoa/__init__.py +62 -51
- pyglet/window/cocoa/pyglet_delegate.py +2 -25
- pyglet/window/cocoa/pyglet_view.py +9 -8
- pyglet/window/dialog/__init__.py +184 -0
- pyglet/window/dialog/base.py +99 -0
- pyglet/window/dialog/darwin.py +121 -0
- pyglet/window/dialog/linux.py +72 -0
- pyglet/window/dialog/windows.py +194 -0
- pyglet/window/emscripten/__init__.py +779 -0
- pyglet/window/headless/__init__.py +44 -28
- pyglet/window/key.py +2 -0
- pyglet/window/mouse.py +2 -2
- pyglet/window/wayland/__init__.py +377 -0
- pyglet/window/win32/__init__.py +101 -46
- pyglet/window/xlib/__init__.py +104 -66
- {pyglet-2.1.12.dist-info → pyglet-3.0.dev1.dist-info}/METADATA +2 -3
- pyglet-3.0.dev1.dist-info/RECORD +322 -0
- {pyglet-2.1.12.dist-info → pyglet-3.0.dev1.dist-info}/WHEEL +1 -1
- pyglet/gl/__init__.py +0 -208
- pyglet/gl/base.py +0 -499
- pyglet/gl/cocoa.py +0 -309
- pyglet/gl/gl.py +0 -4625
- pyglet/gl/gl.pyi +0 -2320
- pyglet/gl/gl_compat.pyi +0 -3097
- pyglet/gl/gl_info.py +0 -190
- pyglet/gl/headless.py +0 -166
- pyglet/gl/wgl_info.py +0 -36
- pyglet/gl/wglext_nv.py +0 -1096
- pyglet/gl/win32.py +0 -268
- pyglet/gl/xlib.py +0 -295
- pyglet/image/buffer.py +0 -274
- pyglet/image/codecs/s3tc.py +0 -354
- pyglet/libs/x11/xrandr.py +0 -166
- pyglet-2.1.12.dist-info/RECORD +0 -234
- /pyglet/{libs/wayland → graphics/api/gl/cocoa}/__init__.py +0 -0
- /pyglet/libs/{egl → linux/egl}/egl.py +0 -0
- /pyglet/libs/{egl → linux/egl}/lib.py +0 -0
- /pyglet/libs/{ioctl.py → linux/ioctl.py} +0 -0
- /pyglet/libs/{wayland → linux/wayland}/gbm.py +0 -0
- /pyglet/libs/{x11 → linux/x11}/__init__.py +0 -0
- /pyglet/libs/{x11 → linux/x11}/cursorfont.py +0 -0
- /pyglet/libs/{x11 → linux/x11}/xlib.py +0 -0
- /pyglet/libs/{x11 → linux/x11}/xsync.py +0 -0
- {pyglet-2.1.12.dist-info/licenses → pyglet-3.0.dev1.dist-info}/LICENSE +0 -0
pyglet/image/base.py
ADDED
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import warnings
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any, BinaryIO, Callable, Generic, Iterator, Sequence, TypeVar, Union
|
|
8
|
+
|
|
9
|
+
from pyglet.customtypes import DataTypes
|
|
10
|
+
from pyglet.image.animation import Animation
|
|
11
|
+
from pyglet.image.codecs import ImageEncoder
|
|
12
|
+
from pyglet.image.codecs import registry as _codec_registry
|
|
13
|
+
from pyglet.util import asbytes
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pyglet.graphics.texture import CompressedTexture, Texture, TextureGrid, TextureSequence
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ImagePattern(ABC):
|
|
20
|
+
"""Abstract image creation class."""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def create_image(self, width: int, height: int) -> _AbstractImage:
|
|
24
|
+
"""Create an image of the given size."""
|
|
25
|
+
raise NotImplementedError('method must be defined in subclass')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _color_as_bytes(color: Sequence[int, int, int, int]) -> bytes:
|
|
29
|
+
if len(color) != 4:
|
|
30
|
+
raise TypeError("color is expected to have 4 components")
|
|
31
|
+
return bytes(color)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ImageException(Exception):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _AbstractImage(ABC):
|
|
39
|
+
"""Abstract class representing an image."""
|
|
40
|
+
|
|
41
|
+
anchor_x: int = 0
|
|
42
|
+
"""X coordinate of anchor, relative to left edge of image data."""
|
|
43
|
+
|
|
44
|
+
anchor_y: int = 0
|
|
45
|
+
"""Y coordinate of anchor, relative to bottom edge of image data."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, width: int, height: int) -> None:
|
|
48
|
+
"""Initialized in subclass."""
|
|
49
|
+
self.width = width
|
|
50
|
+
self.height = height
|
|
51
|
+
|
|
52
|
+
def __repr__(self) -> str:
|
|
53
|
+
return f"{self.__class__.__name__}(size={self.width}x{self.height})"
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def get_image_data(self) -> ImageData:
|
|
57
|
+
"""Get an ImageData view of this image.
|
|
58
|
+
|
|
59
|
+
Changes to the returned instance may or may not be reflected in this image.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def get_region(self, x: int, y: int, width: int, height: int) -> _AbstractImage:
|
|
64
|
+
"""Retrieve a rectangular region of this image."""
|
|
65
|
+
|
|
66
|
+
def save(
|
|
67
|
+
self, filename: str | None = None, file: BinaryIO | None = None, encoder: ImageEncoder | None = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Save this image to a file.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
filename:
|
|
73
|
+
Used to set the image file format, and to open the output file
|
|
74
|
+
if ``file`` is unspecified.
|
|
75
|
+
file:
|
|
76
|
+
An optional file-like object to write image data to.
|
|
77
|
+
encoder:
|
|
78
|
+
If unspecified, all encoders matching the filename extension
|
|
79
|
+
are tried, or all encoders if no filename is provided. If all
|
|
80
|
+
fail, the exception from the first one attempted is raised.
|
|
81
|
+
"""
|
|
82
|
+
if file is None:
|
|
83
|
+
assert filename is not None, "Either filename or file must be specified."
|
|
84
|
+
file = open(filename, 'wb')
|
|
85
|
+
|
|
86
|
+
if encoder is not None:
|
|
87
|
+
encoder.encode(self, filename, file)
|
|
88
|
+
else:
|
|
89
|
+
_codec_registry.encode(self, filename, file)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class _AbstractImageSequence(ABC):
|
|
93
|
+
"""Abstract sequence of images.
|
|
94
|
+
|
|
95
|
+
Image sequence are useful for storing image animations or slices of a volume.
|
|
96
|
+
The class implements the sequence interface (``__len__``, ``__getitem__``,
|
|
97
|
+
``__setitem__``).
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
def get_texture_sequence(self) -> TextureSequence:
|
|
102
|
+
"""Get a TextureSequence.
|
|
103
|
+
|
|
104
|
+
:rtype: `TextureSequence`
|
|
105
|
+
|
|
106
|
+
.. versionadded:: 1.1
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def get_animation(self, period: float, loop: bool = True) -> Animation:
|
|
110
|
+
"""Create an animation over this image sequence for the given constant framerate.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
period:
|
|
114
|
+
Number of seconds to display each frame.
|
|
115
|
+
loop:
|
|
116
|
+
If True, the animation will loop continuously.
|
|
117
|
+
"""
|
|
118
|
+
return Animation.from_image_sequence(self, period, loop)
|
|
119
|
+
|
|
120
|
+
@abstractmethod
|
|
121
|
+
def __getitem__(self, item) -> _AbstractImage:
|
|
122
|
+
"""Retrieve one or more images by index."""
|
|
123
|
+
|
|
124
|
+
@abstractmethod
|
|
125
|
+
def __setitem__(self, item, image: _AbstractImage) -> _AbstractImage:
|
|
126
|
+
"""Replace one or more images in the sequence.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
image:
|
|
130
|
+
The replacement image. The actual instance may not be used,
|
|
131
|
+
depending on this implementation.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
@abstractmethod
|
|
135
|
+
def __len__(self) -> int:
|
|
136
|
+
"""Length of the image sequence."""
|
|
137
|
+
|
|
138
|
+
@abstractmethod
|
|
139
|
+
def __iter__(self) -> Iterator[_AbstractImage]:
|
|
140
|
+
"""Iterate over the images in sequence."""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ImageData(_AbstractImage):
|
|
144
|
+
"""An image represented as a string of unsigned bytes."""
|
|
145
|
+
|
|
146
|
+
_swap1_pattern = re.compile(asbytes('(.)'), re.DOTALL)
|
|
147
|
+
_swap2_pattern = re.compile(asbytes('(.)(.)'), re.DOTALL)
|
|
148
|
+
_swap3_pattern = re.compile(asbytes('(.)(.)(.)'), re.DOTALL)
|
|
149
|
+
_swap4_pattern = re.compile(asbytes('(.)(.)(.)(.)'), re.DOTALL)
|
|
150
|
+
|
|
151
|
+
_current_texture = None
|
|
152
|
+
|
|
153
|
+
def __init__(self, width: int, height: int, fmt: str, data: bytes, pitch: int | None = None,
|
|
154
|
+
data_type: DataTypes = "B") -> None:
|
|
155
|
+
"""Initialize image data.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
width:
|
|
159
|
+
Width of image data
|
|
160
|
+
height:
|
|
161
|
+
Height of image data
|
|
162
|
+
fmt:
|
|
163
|
+
A valid format string, such as 'RGB', 'RGBA', 'ARGB', etc.
|
|
164
|
+
data:
|
|
165
|
+
A sequence of bytes containing the raw image data.
|
|
166
|
+
pitch:
|
|
167
|
+
If specified, the number of bytes per row. Negative values
|
|
168
|
+
indicate a top-to-bottom arrangement. Defaults to ``width * len(format)``.
|
|
169
|
+
data_type:
|
|
170
|
+
The data type of the underlying data. Defaults to unsigned bytes.
|
|
171
|
+
"""
|
|
172
|
+
super().__init__(width, height)
|
|
173
|
+
|
|
174
|
+
self._current_format = self._desired_format = fmt.upper()
|
|
175
|
+
self._data_type = data_type
|
|
176
|
+
self._current_data = data
|
|
177
|
+
self.pitch = pitch or width * len(fmt)
|
|
178
|
+
self._current_pitch = self.pitch
|
|
179
|
+
|
|
180
|
+
def __getstate__(self) -> dict:
|
|
181
|
+
return {
|
|
182
|
+
'width': self.width,
|
|
183
|
+
'height': self.height,
|
|
184
|
+
'_current_data': self.get_bytes(self._current_format, self._current_pitch),
|
|
185
|
+
'_current_format': self._current_format,
|
|
186
|
+
'_desired_format': self._desired_format,
|
|
187
|
+
'_current_pitch': self._current_pitch,
|
|
188
|
+
'_data_type': self._data_type,
|
|
189
|
+
'pitch': self.pitch,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def data_type(self) -> str:
|
|
194
|
+
return self._data_type
|
|
195
|
+
|
|
196
|
+
def get_image_data(self) -> ImageData:
|
|
197
|
+
return self
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def format(self) -> str:
|
|
201
|
+
"""Format string of the data. Read-write."""
|
|
202
|
+
return self._desired_format
|
|
203
|
+
|
|
204
|
+
@format.setter
|
|
205
|
+
def format(self, fmt: str) -> None:
|
|
206
|
+
self._desired_format = fmt.upper()
|
|
207
|
+
self._current_texture = None
|
|
208
|
+
|
|
209
|
+
def get_bytes(self, fmt: str | None = None, pitch: int | None = None) -> bytes:
|
|
210
|
+
"""Get the byte data of the image.
|
|
211
|
+
|
|
212
|
+
This method returns the raw byte data of the image, with optional conversion.
|
|
213
|
+
To convert the data into another format, you can provide ``fmt`` and ``pitch``
|
|
214
|
+
arguments. For example, if the image format is ``RGBA``, and you wish to get
|
|
215
|
+
the byte data in ``RGB`` format::
|
|
216
|
+
|
|
217
|
+
rgb_pitch = my_image.width // len('RGB')
|
|
218
|
+
rgb_img_bytes = my_image.get_bytes(fmt='RGB', pitch=rgb_pitch)
|
|
219
|
+
|
|
220
|
+
The image ``pitch`` may be negative, so be sure to check that when converting
|
|
221
|
+
to another format. Switching the sign of the ``pitch`` will cause the image
|
|
222
|
+
to appear "upside-down".
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
fmt:
|
|
226
|
+
If provided, get the data in another format.
|
|
227
|
+
pitch:
|
|
228
|
+
The number of bytes per row. This generally means the length
|
|
229
|
+
of the format string * the number of pixels per row.
|
|
230
|
+
Negative values indicate a top-to-bottom arrangement.
|
|
231
|
+
|
|
232
|
+
Note:
|
|
233
|
+
Conversion to another format is done on the CPU, and can be
|
|
234
|
+
somewhat costly for larger images. Consider performing conversion
|
|
235
|
+
at load time for framerate sensitive applictions.
|
|
236
|
+
"""
|
|
237
|
+
fmt = fmt or self._desired_format
|
|
238
|
+
pitch = pitch or self._current_pitch
|
|
239
|
+
|
|
240
|
+
if fmt == self._current_format and pitch == self._current_pitch:
|
|
241
|
+
return self._current_data
|
|
242
|
+
return self.convert(fmt, pitch)
|
|
243
|
+
|
|
244
|
+
def set_bytes(self, fmt: str, pitch: int, data: bytes) -> None:
|
|
245
|
+
"""Set the byte data of the image.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
fmt:
|
|
249
|
+
The format string of the supplied data.
|
|
250
|
+
For example: "RGB" or "RGBA"
|
|
251
|
+
pitch:
|
|
252
|
+
The number of bytes per row. This generally means the length
|
|
253
|
+
of the format string * the number of pixels per row.
|
|
254
|
+
Negative values indicate a top-to-bottom arrangement.
|
|
255
|
+
data:
|
|
256
|
+
Image data as bytes.
|
|
257
|
+
"""
|
|
258
|
+
self._current_format = fmt
|
|
259
|
+
self._current_pitch = pitch
|
|
260
|
+
self._current_data = data
|
|
261
|
+
self._current_texture = None
|
|
262
|
+
|
|
263
|
+
def get_data(self, fmt: str | None = None, pitch: int | None = None) -> NotImplementedError:
|
|
264
|
+
"""Get the byte data of the image.
|
|
265
|
+
|
|
266
|
+
Warning:
|
|
267
|
+
This method is deprecated and will be removed in the next version.
|
|
268
|
+
Use :py:meth:`~get_bytes` instead.
|
|
269
|
+
"""
|
|
270
|
+
raise NotImplementedError("Removed. Use `get_bytes` instead.")
|
|
271
|
+
|
|
272
|
+
def set_data(self, fmt: str, pitch: int, data: bytes) -> NotImplementedError:
|
|
273
|
+
"""Set the byte data of the image.
|
|
274
|
+
|
|
275
|
+
Warning:
|
|
276
|
+
This method is deprecated and will be removed in the next version.
|
|
277
|
+
Use :py:meth:`~set_bytes` instead.
|
|
278
|
+
"""
|
|
279
|
+
raise NotImplementedError("Removed. Use `set_bytes` instead.")
|
|
280
|
+
|
|
281
|
+
def create_texture(self, cls: type[Texture]) -> Texture:
|
|
282
|
+
"""Given a texture class, create a texture containing this image."""
|
|
283
|
+
texture = cls.create_from_image(self)
|
|
284
|
+
|
|
285
|
+
if self.anchor_x or self.anchor_y:
|
|
286
|
+
texture.anchor_x = self.anchor_x
|
|
287
|
+
texture.anchor_y = self.anchor_y
|
|
288
|
+
|
|
289
|
+
return texture
|
|
290
|
+
|
|
291
|
+
def get_texture(self) -> Texture:
|
|
292
|
+
if not self._current_texture:
|
|
293
|
+
from pyglet.graphics.texture import Texture
|
|
294
|
+
|
|
295
|
+
self._current_texture = self.create_texture(Texture)
|
|
296
|
+
return self._current_texture
|
|
297
|
+
|
|
298
|
+
def get_region(self, x: int, y: int, width: int, height: int) -> ImageDataRegion:
|
|
299
|
+
"""Retrieve a rectangular region of this image data."""
|
|
300
|
+
return ImageDataRegion(x, y, width, height, self)
|
|
301
|
+
|
|
302
|
+
def blit(self, x: int, y: int, z: int = 0, width: int | None = None, height: int | None = None) -> None:
|
|
303
|
+
raise NotImplementedError("This is no longer supported. Check documentation for 3.0 migration.")
|
|
304
|
+
|
|
305
|
+
def convert(self, fmt: str, pitch: int) -> bytes:
|
|
306
|
+
"""Convert data to the desired format.
|
|
307
|
+
|
|
308
|
+
This method does not alter this instance's current format or pitch.
|
|
309
|
+
|
|
310
|
+
Can be expensive depending on the size of the image and kind of re-ordering.
|
|
311
|
+
"""
|
|
312
|
+
if fmt == self._current_format and pitch == self._current_pitch:
|
|
313
|
+
return self._current_data
|
|
314
|
+
|
|
315
|
+
self._ensure_bytes()
|
|
316
|
+
data = self._current_data
|
|
317
|
+
current_pitch = self._current_pitch
|
|
318
|
+
current_format = self._current_format
|
|
319
|
+
sign_pitch = current_pitch // abs(current_pitch)
|
|
320
|
+
if fmt != self._current_format:
|
|
321
|
+
# Create replacement string, e.g. r'\4\1\2\3' to convert RGBA to ARGB
|
|
322
|
+
repl = asbytes('')
|
|
323
|
+
for c in fmt:
|
|
324
|
+
try:
|
|
325
|
+
idx = current_format.index(c) + 1
|
|
326
|
+
except ValueError:
|
|
327
|
+
idx = 1
|
|
328
|
+
repl += asbytes(r'\%d' % idx)
|
|
329
|
+
|
|
330
|
+
if len(current_format) == 1:
|
|
331
|
+
swap_pattern = self._swap1_pattern
|
|
332
|
+
elif len(current_format) == 2:
|
|
333
|
+
swap_pattern = self._swap2_pattern
|
|
334
|
+
elif len(current_format) == 3:
|
|
335
|
+
swap_pattern = self._swap3_pattern
|
|
336
|
+
elif len(current_format) == 4:
|
|
337
|
+
swap_pattern = self._swap4_pattern
|
|
338
|
+
else:
|
|
339
|
+
raise ImageException('Current image format is wider than 32 bits.')
|
|
340
|
+
|
|
341
|
+
packed_pitch = self.width * len(current_format)
|
|
342
|
+
if abs(self._current_pitch) != packed_pitch:
|
|
343
|
+
# Pitch is wider than pixel data, need to go row-by-row.
|
|
344
|
+
new_pitch = abs(self._current_pitch)
|
|
345
|
+
rows = [data[i : i + new_pitch] for i in range(0, len(data), new_pitch)]
|
|
346
|
+
rows = [swap_pattern.sub(repl, r[:packed_pitch]) for r in rows]
|
|
347
|
+
data = b''.join(rows)
|
|
348
|
+
else:
|
|
349
|
+
# Rows are tightly packed, apply regex over whole image.
|
|
350
|
+
data = swap_pattern.sub(repl, data)
|
|
351
|
+
|
|
352
|
+
# After conversion, rows will always be tightly packed
|
|
353
|
+
current_pitch = sign_pitch * (len(fmt) * self.width)
|
|
354
|
+
|
|
355
|
+
if pitch != current_pitch:
|
|
356
|
+
diff = abs(current_pitch) - abs(pitch)
|
|
357
|
+
if diff > 0:
|
|
358
|
+
# New pitch is shorter than old pitch, chop bytes off each row
|
|
359
|
+
new_pitch = abs(pitch)
|
|
360
|
+
rows = [data[i : i + new_pitch - diff] for i in range(0, len(data), new_pitch)]
|
|
361
|
+
data = b''.join(rows)
|
|
362
|
+
|
|
363
|
+
elif diff < 0:
|
|
364
|
+
# New pitch is longer than old pitch, add '0' bytes to each row
|
|
365
|
+
new_pitch = abs(current_pitch)
|
|
366
|
+
padding = bytes(1) * -diff
|
|
367
|
+
rows = [data[i : i + new_pitch] + padding for i in range(0, len(data), new_pitch)]
|
|
368
|
+
data = b''.join(rows)
|
|
369
|
+
|
|
370
|
+
if current_pitch * pitch < 0:
|
|
371
|
+
# Pitch differs in sign, swap row order
|
|
372
|
+
new_pitch = abs(pitch)
|
|
373
|
+
rows = [data[i : i + new_pitch] for i in range(0, len(data), new_pitch)]
|
|
374
|
+
rows.reverse()
|
|
375
|
+
data = b''.join(rows)
|
|
376
|
+
|
|
377
|
+
return data
|
|
378
|
+
|
|
379
|
+
def _ensure_bytes(self) -> None:
|
|
380
|
+
if type(self._current_data) is not bytes:
|
|
381
|
+
self._current_data = asbytes(self._current_data)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class ImageDataRegion(ImageData):
|
|
385
|
+
"""A region of image data taken from an existing ImageData or region."""
|
|
386
|
+
|
|
387
|
+
def __init__(self, x: int, y: int, width: int, height: int, image_data: ImageData) -> None: # noqa: D107
|
|
388
|
+
super().__init__(width, height, image_data._current_format, image_data._current_data, image_data._current_pitch) # noqa: SLF001
|
|
389
|
+
self.x = x
|
|
390
|
+
self.y = y
|
|
391
|
+
|
|
392
|
+
def __getstate__(self) -> dict:
|
|
393
|
+
return {
|
|
394
|
+
'width': self.width,
|
|
395
|
+
'height': self.height,
|
|
396
|
+
'_current_data': self.get_bytes(self._current_format, self._current_pitch),
|
|
397
|
+
'_current_format': self._current_format,
|
|
398
|
+
'_desired_format': self._desired_format,
|
|
399
|
+
'_current_pitch': self._current_pitch,
|
|
400
|
+
'pitch': self.pitch,
|
|
401
|
+
'x': self.x,
|
|
402
|
+
'y': self.y,
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
def get_bytes(self, fmt: str | None = None, pitch: int | None = None) -> bytes:
|
|
406
|
+
x1 = len(self._current_format) * self.x
|
|
407
|
+
x2 = len(self._current_format) * (self.x + self.width)
|
|
408
|
+
|
|
409
|
+
self._ensure_bytes()
|
|
410
|
+
data = self.convert(self._current_format, abs(self._current_pitch))
|
|
411
|
+
new_pitch = abs(self._current_pitch)
|
|
412
|
+
rows = [data[i : i + new_pitch] for i in range(0, len(data), new_pitch)]
|
|
413
|
+
rows = [row[x1:x2] for row in rows[self.y : self.y + self.height]]
|
|
414
|
+
self._current_data = b''.join(rows)
|
|
415
|
+
self._current_pitch = self.width * len(self._current_format)
|
|
416
|
+
self._current_texture = None
|
|
417
|
+
self.x = 0
|
|
418
|
+
self.y = 0
|
|
419
|
+
|
|
420
|
+
fmt = fmt or self._desired_format
|
|
421
|
+
pitch = pitch or self._current_pitch
|
|
422
|
+
return super().get_bytes(fmt, pitch)
|
|
423
|
+
|
|
424
|
+
def set_bytes(self, fmt: str, pitch: int, data: bytes) -> None:
|
|
425
|
+
self.x = 0
|
|
426
|
+
self.y = 0
|
|
427
|
+
super().set_bytes(fmt, pitch, data)
|
|
428
|
+
|
|
429
|
+
def get_region(self, x: int, y: int, width: int, height: int) -> ImageDataRegion:
|
|
430
|
+
x += self.x
|
|
431
|
+
y += self.y
|
|
432
|
+
return super().get_region(x, y, width, height)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@dataclass(frozen=True)
|
|
436
|
+
class CompressionFormat:
|
|
437
|
+
fmt: bytes
|
|
438
|
+
alpha: bool
|
|
439
|
+
dxgi_format: int = 0
|
|
440
|
+
vk_format: int = 0
|
|
441
|
+
|
|
442
|
+
class CompressedImageData(_AbstractImage):
|
|
443
|
+
"""Compressed image data suitable for direct uploading to GPU."""
|
|
444
|
+
|
|
445
|
+
_current_texture = None
|
|
446
|
+
|
|
447
|
+
def __init__(
|
|
448
|
+
self,
|
|
449
|
+
width: int,
|
|
450
|
+
height: int,
|
|
451
|
+
fmt: CompressionFormat,
|
|
452
|
+
data: bytes,
|
|
453
|
+
extension: str | None = None,
|
|
454
|
+
decoder: Callable[[bytes, int, int], _AbstractImage] | None = None,
|
|
455
|
+
) -> None:
|
|
456
|
+
"""Construct a CompressedImageData with the given compressed data.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
width:
|
|
460
|
+
The width of the image.
|
|
461
|
+
height:
|
|
462
|
+
The height of the image.
|
|
463
|
+
data:
|
|
464
|
+
An array of bytes containing the compressed image data.
|
|
465
|
+
extension:
|
|
466
|
+
If specified, gives the name of the extension to check for
|
|
467
|
+
before creating a texture.
|
|
468
|
+
decoder:
|
|
469
|
+
An optional fallback function used to decode the compressed data.
|
|
470
|
+
This function is called if the required extension is not present.
|
|
471
|
+
"""
|
|
472
|
+
super().__init__(width, height)
|
|
473
|
+
self.data = data
|
|
474
|
+
self.fmt = fmt
|
|
475
|
+
self.extension = extension
|
|
476
|
+
self.decoder = decoder
|
|
477
|
+
self.data_type = "B"
|
|
478
|
+
self.mipmap_data: list[bytes | None] = []
|
|
479
|
+
|
|
480
|
+
def _have_extension(self) -> bool:
|
|
481
|
+
raise NotImplementedError
|
|
482
|
+
|
|
483
|
+
def set_mipmap_data(self, level: int, data: bytes) -> None:
|
|
484
|
+
"""Set compressed image data for a mipmap level.
|
|
485
|
+
|
|
486
|
+
Supplied data gives a compressed image for the given mipmap level.
|
|
487
|
+
This image data must be in the same format as was used in the
|
|
488
|
+
constructor. The image data must also be of the correct dimensions for
|
|
489
|
+
the level (i.e., width >> level, height >> level); but this is not checked.
|
|
490
|
+
If *any* mipmap levels are specified, they are used; otherwise, mipmaps for
|
|
491
|
+
``mipmapped_texture`` are generated automatically.
|
|
492
|
+
"""
|
|
493
|
+
# Extend mipmap_data list to required level
|
|
494
|
+
self.mipmap_data += [None] * (level - len(self.mipmap_data))
|
|
495
|
+
self.mipmap_data[level - 1] = data
|
|
496
|
+
|
|
497
|
+
def create_texture(self, cls: type[CompressedTexture]) -> Texture:
|
|
498
|
+
"""Given a texture class, create a texture containing this image."""
|
|
499
|
+
texture = cls.create_from_image(self)
|
|
500
|
+
|
|
501
|
+
if self.anchor_x or self.anchor_y:
|
|
502
|
+
texture.anchor_x = self.anchor_x
|
|
503
|
+
texture.anchor_y = self.anchor_y
|
|
504
|
+
|
|
505
|
+
return texture
|
|
506
|
+
|
|
507
|
+
def get_texture(self) -> CompressedTexture:
|
|
508
|
+
if not self._current_texture:
|
|
509
|
+
from pyglet.graphics.texture import CompressedTexture
|
|
510
|
+
|
|
511
|
+
self._current_texture = self.create_texture(CompressedTexture)
|
|
512
|
+
return self._current_texture
|
|
513
|
+
|
|
514
|
+
def get_image_data(self) -> CompressedImageData:
|
|
515
|
+
return self
|
|
516
|
+
|
|
517
|
+
def get_region(self, x: int, y: int, width: int, height: int) -> _AbstractImage:
|
|
518
|
+
raise NotImplementedError(f"Not implemented for {self}")
|
|
519
|
+
|
|
520
|
+
def blit(self, x: int, y: int, z: int = 0) -> None:
|
|
521
|
+
raise NotImplementedError
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
T = TypeVar('T', bound=_AbstractImage)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
class _AbstractGrid(ABC, Generic[T]):
|
|
528
|
+
column_padding: int
|
|
529
|
+
row_padding: int
|
|
530
|
+
columns: int
|
|
531
|
+
rows: int
|
|
532
|
+
item_width: int
|
|
533
|
+
item_height: int
|
|
534
|
+
_height: int
|
|
535
|
+
_width: int
|
|
536
|
+
_items: list[T] | None = None
|
|
537
|
+
|
|
538
|
+
def __init__(
|
|
539
|
+
self, rows: int, columns: int, item_width: int, item_height: int, row_padding: int = 0, column_padding: int = 0,
|
|
540
|
+
) -> None:
|
|
541
|
+
self.rows = rows
|
|
542
|
+
self.columns = columns
|
|
543
|
+
self.item_width = item_width
|
|
544
|
+
self.item_height = item_height
|
|
545
|
+
self.row_padding = row_padding
|
|
546
|
+
self.column_padding = column_padding
|
|
547
|
+
self._items = None
|
|
548
|
+
self._width = (item_width * columns) + (column_padding * columns)
|
|
549
|
+
self._height = (item_height * rows) + (row_padding * rows)
|
|
550
|
+
|
|
551
|
+
@property
|
|
552
|
+
def width(self) -> int:
|
|
553
|
+
return self._width
|
|
554
|
+
|
|
555
|
+
@property
|
|
556
|
+
def height(self) -> int:
|
|
557
|
+
return self._height
|
|
558
|
+
|
|
559
|
+
def _generate_items(self) -> list[T]:
|
|
560
|
+
if self._items is None:
|
|
561
|
+
self._items = []
|
|
562
|
+
y = 0
|
|
563
|
+
for row in range(self.rows):
|
|
564
|
+
x = 0
|
|
565
|
+
for col in range(self.columns):
|
|
566
|
+
self._items.append(self._create_item(x, y, self.item_width, self.item_height))
|
|
567
|
+
x += self.item_width + self.column_padding
|
|
568
|
+
y += self.item_height + self.row_padding
|
|
569
|
+
return self._items
|
|
570
|
+
|
|
571
|
+
@abstractmethod
|
|
572
|
+
def _create_item(self, x: int, y: int, width: int, height: int) -> T:
|
|
573
|
+
"""Factory method to create a grid item.
|
|
574
|
+
|
|
575
|
+
Must be implemented by subclasses.
|
|
576
|
+
"""
|
|
577
|
+
|
|
578
|
+
@abstractmethod
|
|
579
|
+
def _update_item(self, existing_item: T, new_item: T) -> None:
|
|
580
|
+
"""Factory method to update an existing grid item.
|
|
581
|
+
|
|
582
|
+
Must be implemented by subclasses, even if it does not change the data.
|
|
583
|
+
"""
|
|
584
|
+
|
|
585
|
+
def __getitem__(self, index: int | tuple[int, int] | slice) -> list[Any] | None | Any:
|
|
586
|
+
items = self._generate_items()
|
|
587
|
+
if isinstance(index, slice):
|
|
588
|
+
if type(index.start) is not tuple and type(index.stop) is not tuple:
|
|
589
|
+
return items[index]
|
|
590
|
+
row1 = 0
|
|
591
|
+
col1 = 0
|
|
592
|
+
row2 = self.rows
|
|
593
|
+
col2 = self.columns
|
|
594
|
+
if type(index.start) is tuple:
|
|
595
|
+
row1, col1 = index.start
|
|
596
|
+
elif type(index.start) is int:
|
|
597
|
+
row1 = index.start // self.columns
|
|
598
|
+
col1 = index.start % self.columns
|
|
599
|
+
assert 0 <= row1 < self.rows and 0 <= col1 < self.columns, "Grid index out of range" # noqa: PT018
|
|
600
|
+
|
|
601
|
+
if type(index.stop) is tuple:
|
|
602
|
+
row2, col2 = index.stop
|
|
603
|
+
elif type(index.stop) is int:
|
|
604
|
+
row2 = index.stop // self.columns
|
|
605
|
+
col2 = index.stop % self.columns
|
|
606
|
+
assert 0 <= row2 <= self.rows and 0 <= col2 <= self.columns, "Grid index out of range" # noqa: PT018
|
|
607
|
+
|
|
608
|
+
result = []
|
|
609
|
+
i = row1 * self.columns
|
|
610
|
+
for row in range(row1, row2):
|
|
611
|
+
result += items[i + col1 : i + col2]
|
|
612
|
+
i += self.columns
|
|
613
|
+
return result
|
|
614
|
+
if isinstance(index, tuple):
|
|
615
|
+
row, column = index
|
|
616
|
+
assert 0 <= row < self.rows and 0 <= column < self.columns, "Grid index out of range" # noqa: PT018
|
|
617
|
+
return items[row * self.columns + column]
|
|
618
|
+
|
|
619
|
+
return items[index]
|
|
620
|
+
|
|
621
|
+
def __setitem__(self, index: int | slice, value: T | Sequence[T]) -> None:
|
|
622
|
+
if isinstance(value, slice):
|
|
623
|
+
for existing_item, new_item in zip(self[index], value):
|
|
624
|
+
if new_item.width != self.item_width or new_item.height != self.item_height:
|
|
625
|
+
msg = (
|
|
626
|
+
f'Dimensions of new item does not match existing.\n'
|
|
627
|
+
f'Expected {self.item_width}x{self.item_height}, Received {new_item.width}x{new_item.height}'
|
|
628
|
+
)
|
|
629
|
+
raise ImageException(msg)
|
|
630
|
+
self._update_item(existing_item, new_item)
|
|
631
|
+
else:
|
|
632
|
+
new_item = value
|
|
633
|
+
if new_item.width != self.item_width or new_item.height != self.item_height:
|
|
634
|
+
msg = (
|
|
635
|
+
f'Dimensions of new item does not match existing.\n'
|
|
636
|
+
f'Expected {self.item_width}x{self.item_height}, Received {new_item.width}x{new_item.height}'
|
|
637
|
+
)
|
|
638
|
+
raise ImageException(msg)
|
|
639
|
+
self._update_item(self[index], new_item)
|
|
640
|
+
|
|
641
|
+
def __iter__(self) -> Iterator[T]:
|
|
642
|
+
return iter(self._generate_items())
|
|
643
|
+
|
|
644
|
+
def __len__(self) -> int:
|
|
645
|
+
return self.rows * self.columns
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class ImageGrid(_AbstractGrid[Union[ImageData, ImageDataRegion]], _AbstractImageSequence):
|
|
649
|
+
"""An imaginary grid placed over an image allowing easy access to regular regions of that image.
|
|
650
|
+
|
|
651
|
+
The grid can be accessed either as a complete image, or as a sequence of images.
|
|
652
|
+
|
|
653
|
+
Any :py:class:`~pyglet.graphics.TextureGrid` generated via the method below will create
|
|
654
|
+
a separate texture resource::
|
|
655
|
+
|
|
656
|
+
image_grid = ImageGrid(...)
|
|
657
|
+
texture_grid = image_grid.get_texture_sequence()
|
|
658
|
+
|
|
659
|
+
For existing Texture's, it is recommended to use :py:class:`~pyglet.graphics.TextureGrid` directly.
|
|
660
|
+
"""
|
|
661
|
+
|
|
662
|
+
_texture_grid: TextureGrid | None = None
|
|
663
|
+
|
|
664
|
+
def __init__(
|
|
665
|
+
self,
|
|
666
|
+
image: ImageData | ImageDataRegion,
|
|
667
|
+
rows: int,
|
|
668
|
+
columns: int,
|
|
669
|
+
item_width: int | None = None,
|
|
670
|
+
item_height: int | None = None,
|
|
671
|
+
row_padding: int = 0,
|
|
672
|
+
column_padding: int = 0,
|
|
673
|
+
) -> None:
|
|
674
|
+
"""Construct a grid for the given image.
|
|
675
|
+
|
|
676
|
+
You can specify parameters for the grid, for example setting
|
|
677
|
+
the padding between cells. Grids are always aligned to the
|
|
678
|
+
bottom-left corner of the image.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
image:
|
|
682
|
+
Image over which to construct the grid.
|
|
683
|
+
rows:
|
|
684
|
+
Number of rows in the grid.
|
|
685
|
+
columns:
|
|
686
|
+
Number of columns in the grid.
|
|
687
|
+
item_width:
|
|
688
|
+
Width of each column. If unspecified, is calculated such
|
|
689
|
+
that the entire image width is used.
|
|
690
|
+
item_height:
|
|
691
|
+
Height of each row. If unspecified, is calculated such that
|
|
692
|
+
the entire image height is used.
|
|
693
|
+
row_padding:
|
|
694
|
+
Pixels separating adjacent rows. The padding is only
|
|
695
|
+
inserted between rows, not at the edges of the grid.
|
|
696
|
+
column_padding:
|
|
697
|
+
Pixels separating adjacent columns. The padding is only
|
|
698
|
+
inserted between columns, not at the edges of the grid.
|
|
699
|
+
"""
|
|
700
|
+
item_width = item_width or (image.width - column_padding * (columns - 1)) // columns
|
|
701
|
+
item_height = item_height or (image.height - row_padding * (rows - 1)) // rows
|
|
702
|
+
super().__init__(rows, columns, item_width, item_height, row_padding, column_padding)
|
|
703
|
+
self.image = image
|
|
704
|
+
|
|
705
|
+
def _create_item(self, x: int, y: int, width: int, height: int) -> ImageDataRegion:
|
|
706
|
+
return self.image.get_region(x, y, width, height)
|
|
707
|
+
|
|
708
|
+
def _update_item(self, existing_item: ImageDataRegion, new_item: ImageData | ImageDataRegion) -> None:
|
|
709
|
+
new_item_bytes = new_item.get_bytes(existing_item.format, existing_item.pitch)
|
|
710
|
+
existing_item.set_data(existing_item.format, existing_item.pitch, new_item_bytes)
|
|
711
|
+
|
|
712
|
+
def get_texture(self) -> Texture:
|
|
713
|
+
"""Create a new Texture resource from the underlying image data."""
|
|
714
|
+
return self.image.get_texture()
|
|
715
|
+
|
|
716
|
+
def get_image_data(self) -> ImageData:
|
|
717
|
+
"""Retrieve the underlying image data the grid is based upon."""
|
|
718
|
+
return self.image.get_image_data()
|
|
719
|
+
|
|
720
|
+
def get_texture_sequence(self) -> TextureGrid:
|
|
721
|
+
"""Create a :py:class:`~pyglet.graphics.TextureGrid` resource from the underlying image data.
|
|
722
|
+
|
|
723
|
+
It is recommended to use :py:class:`~pyglet.graphics.TextureGrid` directly.
|
|
724
|
+
"""
|
|
725
|
+
warnings.warn("Use pyglet.graphics.TextureGrid instead", DeprecationWarning)
|
|
726
|
+
if not self._texture_grid:
|
|
727
|
+
from pyglet.graphics.texture import TextureGrid
|
|
728
|
+
self._texture_grid = TextureGrid.from_image_grid(self)
|
|
729
|
+
return self._texture_grid
|
|
730
|
+
|