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.
Files changed (267) hide show
  1. pyglet/__init__.py +67 -61
  2. pyglet/__init__.pyi +15 -8
  3. pyglet/app/__init__.py +22 -13
  4. pyglet/app/async_app.py +212 -0
  5. pyglet/app/base.py +2 -1
  6. pyglet/app/{xlib.py → linux.py} +3 -3
  7. pyglet/config/__init__.py +101 -0
  8. pyglet/config/gl/__init__.py +30 -0
  9. pyglet/config/gl/egl.py +120 -0
  10. pyglet/config/gl/macos.py +262 -0
  11. pyglet/config/gl/windows.py +267 -0
  12. pyglet/config/gl/x11.py +142 -0
  13. pyglet/customtypes.py +43 -2
  14. pyglet/display/__init__.py +8 -6
  15. pyglet/display/base.py +3 -63
  16. pyglet/display/cocoa.py +12 -17
  17. pyglet/display/emscripten.py +39 -0
  18. pyglet/display/headless.py +23 -30
  19. pyglet/display/wayland.py +157 -0
  20. pyglet/display/win32.py +4 -17
  21. pyglet/display/xlib.py +19 -27
  22. pyglet/display/xlib_vidmoderestore.py +2 -2
  23. pyglet/enums.py +183 -0
  24. pyglet/event.py +0 -1
  25. pyglet/experimental/geoshader_sprite.py +15 -13
  26. pyglet/experimental/hidraw.py +6 -15
  27. pyglet/experimental/multitexture_sprite.py +31 -19
  28. pyglet/experimental/particles.py +13 -35
  29. pyglet/font/__init__.py +251 -85
  30. pyglet/font/base.py +116 -61
  31. pyglet/font/dwrite/__init__.py +349 -204
  32. pyglet/font/dwrite/dwrite_lib.py +27 -5
  33. pyglet/font/fontconfig.py +14 -6
  34. pyglet/font/freetype.py +138 -87
  35. pyglet/font/freetype_lib.py +19 -0
  36. pyglet/font/group.py +179 -0
  37. pyglet/font/harfbuzz/__init__.py +3 -3
  38. pyglet/font/pyodide_js.py +310 -0
  39. pyglet/font/quartz.py +319 -126
  40. pyglet/font/ttf.py +45 -3
  41. pyglet/font/user.py +14 -19
  42. pyglet/font/win32.py +45 -21
  43. pyglet/graphics/__init__.py +8 -787
  44. pyglet/graphics/allocation.py +115 -1
  45. pyglet/graphics/api/__init__.py +77 -0
  46. pyglet/graphics/api/base.py +299 -0
  47. pyglet/graphics/api/gl/__init__.py +58 -0
  48. pyglet/graphics/api/gl/base.py +24 -0
  49. pyglet/graphics/{vertexbuffer.py → api/gl/buffer.py} +104 -159
  50. pyglet/graphics/api/gl/cocoa/context.py +76 -0
  51. pyglet/graphics/api/gl/context.py +391 -0
  52. pyglet/graphics/api/gl/default_shaders.py +0 -0
  53. pyglet/graphics/api/gl/draw.py +627 -0
  54. pyglet/graphics/api/gl/egl/__init__.py +0 -0
  55. pyglet/graphics/api/gl/egl/context.py +92 -0
  56. pyglet/graphics/api/gl/enums.py +76 -0
  57. pyglet/graphics/api/gl/framebuffer.py +315 -0
  58. pyglet/graphics/api/gl/gl.py +5463 -0
  59. pyglet/graphics/api/gl/gl_info.py +188 -0
  60. pyglet/graphics/api/gl/global_opengl.py +226 -0
  61. pyglet/{gl → graphics/api/gl}/lib.py +34 -18
  62. pyglet/graphics/api/gl/shader.py +1476 -0
  63. pyglet/graphics/api/gl/shapes.py +55 -0
  64. pyglet/graphics/api/gl/sprite.py +102 -0
  65. pyglet/graphics/api/gl/state.py +219 -0
  66. pyglet/graphics/api/gl/text.py +190 -0
  67. pyglet/graphics/api/gl/texture.py +1526 -0
  68. pyglet/graphics/{vertexarray.py → api/gl/vertexarray.py} +11 -13
  69. pyglet/graphics/api/gl/vertexdomain.py +751 -0
  70. pyglet/graphics/api/gl/win32/__init__.py +0 -0
  71. pyglet/graphics/api/gl/win32/context.py +108 -0
  72. pyglet/graphics/api/gl/win32/wgl_info.py +24 -0
  73. pyglet/graphics/api/gl/xlib/__init__.py +0 -0
  74. pyglet/graphics/api/gl/xlib/context.py +174 -0
  75. pyglet/{gl → graphics/api/gl/xlib}/glx_info.py +26 -31
  76. pyglet/graphics/api/gl1/__init__.py +0 -0
  77. pyglet/{gl → graphics/api/gl1}/gl_compat.py +3 -2
  78. pyglet/graphics/api/gl2/__init__.py +0 -0
  79. pyglet/graphics/api/gl2/buffer.py +320 -0
  80. pyglet/graphics/api/gl2/draw.py +600 -0
  81. pyglet/graphics/api/gl2/global_opengl.py +122 -0
  82. pyglet/graphics/api/gl2/shader.py +200 -0
  83. pyglet/graphics/api/gl2/shapes.py +51 -0
  84. pyglet/graphics/api/gl2/sprite.py +79 -0
  85. pyglet/graphics/api/gl2/text.py +175 -0
  86. pyglet/graphics/api/gl2/vertexdomain.py +364 -0
  87. pyglet/graphics/api/webgl/__init__.py +233 -0
  88. pyglet/graphics/api/webgl/buffer.py +302 -0
  89. pyglet/graphics/api/webgl/context.py +234 -0
  90. pyglet/graphics/api/webgl/draw.py +590 -0
  91. pyglet/graphics/api/webgl/enums.py +76 -0
  92. pyglet/graphics/api/webgl/framebuffer.py +360 -0
  93. pyglet/graphics/api/webgl/gl.py +1537 -0
  94. pyglet/graphics/api/webgl/gl_info.py +130 -0
  95. pyglet/graphics/api/webgl/shader.py +1346 -0
  96. pyglet/graphics/api/webgl/shapes.py +92 -0
  97. pyglet/graphics/api/webgl/sprite.py +102 -0
  98. pyglet/graphics/api/webgl/state.py +227 -0
  99. pyglet/graphics/api/webgl/text.py +187 -0
  100. pyglet/graphics/api/webgl/texture.py +1227 -0
  101. pyglet/graphics/api/webgl/vertexarray.py +54 -0
  102. pyglet/graphics/api/webgl/vertexdomain.py +616 -0
  103. pyglet/graphics/api/webgl/webgl_js.pyi +307 -0
  104. pyglet/{image → graphics}/atlas.py +33 -32
  105. pyglet/graphics/base.py +10 -0
  106. pyglet/graphics/buffer.py +245 -0
  107. pyglet/graphics/draw.py +578 -0
  108. pyglet/graphics/framebuffer.py +26 -0
  109. pyglet/graphics/instance.py +178 -69
  110. pyglet/graphics/shader.py +267 -1553
  111. pyglet/graphics/state.py +83 -0
  112. pyglet/graphics/texture.py +703 -0
  113. pyglet/graphics/vertexdomain.py +695 -538
  114. pyglet/gui/ninepatch.py +10 -10
  115. pyglet/gui/widgets.py +120 -10
  116. pyglet/image/__init__.py +20 -1973
  117. pyglet/image/animation.py +12 -12
  118. pyglet/image/base.py +730 -0
  119. pyglet/image/codecs/__init__.py +9 -0
  120. pyglet/image/codecs/bmp.py +53 -30
  121. pyglet/image/codecs/dds.py +53 -31
  122. pyglet/image/codecs/gdiplus.py +38 -14
  123. pyglet/image/codecs/gdkpixbuf2.py +0 -2
  124. pyglet/image/codecs/js_image.py +99 -0
  125. pyglet/image/codecs/ktx2.py +161 -0
  126. pyglet/image/codecs/pil.py +1 -1
  127. pyglet/image/codecs/png.py +1 -1
  128. pyglet/image/codecs/wic.py +11 -2
  129. pyglet/info.py +26 -24
  130. pyglet/input/__init__.py +8 -0
  131. pyglet/input/base.py +163 -105
  132. pyglet/input/controller.py +13 -19
  133. pyglet/input/controller_db.py +39 -24
  134. pyglet/input/emscripten/__init__.py +18 -0
  135. pyglet/input/emscripten/gamepad_js.py +397 -0
  136. pyglet/input/linux/__init__.py +11 -5
  137. pyglet/input/linux/evdev.py +10 -11
  138. pyglet/input/linux/x11_xinput.py +2 -2
  139. pyglet/input/linux/x11_xinput_tablet.py +1 -1
  140. pyglet/input/macos/__init__.py +7 -2
  141. pyglet/input/macos/darwin_gc.py +559 -0
  142. pyglet/input/win32/__init__.py +1 -1
  143. pyglet/input/win32/directinput.py +34 -29
  144. pyglet/input/win32/xinput.py +11 -61
  145. pyglet/lib.py +3 -3
  146. pyglet/libs/__init__.py +1 -1
  147. pyglet/{gl → libs/darwin}/agl.py +1 -1
  148. pyglet/libs/darwin/cocoapy/__init__.py +2 -2
  149. pyglet/libs/darwin/cocoapy/cocoahelpers.py +181 -0
  150. pyglet/libs/darwin/cocoapy/cocoalibs.py +31 -0
  151. pyglet/libs/darwin/cocoapy/cocoatypes.py +27 -0
  152. pyglet/libs/darwin/cocoapy/runtime.py +81 -45
  153. pyglet/libs/darwin/coreaudio.py +4 -4
  154. pyglet/{gl → libs/darwin}/lib_agl.py +9 -8
  155. pyglet/libs/darwin/quartzkey.py +1 -3
  156. pyglet/libs/egl/__init__.py +2 -0
  157. pyglet/libs/egl/egl_lib.py +576 -0
  158. pyglet/libs/egl/eglext.py +51 -5
  159. pyglet/libs/linux/__init__.py +0 -0
  160. pyglet/libs/linux/egl/__init__.py +0 -0
  161. pyglet/libs/linux/egl/eglext.py +22 -0
  162. pyglet/libs/linux/glx/__init__.py +0 -0
  163. pyglet/{gl → libs/linux/glx}/glx.py +13 -14
  164. pyglet/{gl → libs/linux/glx}/glxext_arb.py +408 -192
  165. pyglet/{gl → libs/linux/glx}/glxext_mesa.py +1 -1
  166. pyglet/{gl → libs/linux/glx}/glxext_nv.py +345 -164
  167. pyglet/{gl → libs/linux/glx}/lib_glx.py +3 -2
  168. pyglet/libs/linux/wayland/__init__.py +0 -0
  169. pyglet/libs/linux/wayland/client.py +1068 -0
  170. pyglet/libs/linux/wayland/lib_wayland.py +207 -0
  171. pyglet/libs/linux/wayland/wayland_egl.py +38 -0
  172. pyglet/libs/{wayland → linux/wayland}/xkbcommon.py +26 -0
  173. pyglet/libs/{x11 → linux/x11}/xf86vmode.py +4 -4
  174. pyglet/libs/{x11 → linux/x11}/xinerama.py +2 -2
  175. pyglet/libs/{x11 → linux/x11}/xinput.py +10 -10
  176. pyglet/libs/linux/x11/xrandr.py +0 -0
  177. pyglet/libs/{x11 → linux/x11}/xrender.py +1 -1
  178. pyglet/libs/shared/__init__.py +0 -0
  179. pyglet/libs/shared/spirv/__init__.py +0 -0
  180. pyglet/libs/shared/spirv/lib_shaderc.py +85 -0
  181. pyglet/libs/shared/spirv/lib_spirv_cross.py +126 -0
  182. pyglet/libs/win32/__init__.py +27 -5
  183. pyglet/libs/win32/constants.py +59 -48
  184. pyglet/libs/win32/context_managers.py +20 -3
  185. pyglet/libs/win32/dinput.py +105 -88
  186. pyglet/{gl → libs/win32}/lib_wgl.py +52 -26
  187. pyglet/libs/win32/types.py +58 -23
  188. pyglet/{gl → libs/win32}/wgl.py +32 -25
  189. pyglet/{gl → libs/win32}/wglext_arb.py +364 -2
  190. pyglet/media/__init__.py +9 -10
  191. pyglet/media/codecs/__init__.py +12 -1
  192. pyglet/media/codecs/base.py +99 -96
  193. pyglet/media/codecs/ffmpeg.py +2 -2
  194. pyglet/media/codecs/ffmpeg_lib/libavformat.py +3 -8
  195. pyglet/media/codecs/webaudio_pyodide.py +111 -0
  196. pyglet/media/drivers/__init__.py +9 -4
  197. pyglet/media/drivers/base.py +4 -4
  198. pyglet/media/drivers/openal/__init__.py +1 -1
  199. pyglet/media/drivers/openal/adaptation.py +3 -3
  200. pyglet/media/drivers/pulse/__init__.py +1 -1
  201. pyglet/media/drivers/pulse/adaptation.py +3 -3
  202. pyglet/media/drivers/pyodide_js/__init__.py +8 -0
  203. pyglet/media/drivers/pyodide_js/adaptation.py +288 -0
  204. pyglet/media/drivers/xaudio2/adaptation.py +3 -3
  205. pyglet/media/player.py +276 -193
  206. pyglet/media/player_worker_thread.py +1 -1
  207. pyglet/model/__init__.py +39 -29
  208. pyglet/model/codecs/base.py +4 -4
  209. pyglet/model/codecs/gltf.py +3 -3
  210. pyglet/model/codecs/obj.py +71 -43
  211. pyglet/resource.py +129 -78
  212. pyglet/shapes.py +147 -177
  213. pyglet/sprite.py +47 -164
  214. pyglet/text/__init__.py +44 -54
  215. pyglet/text/caret.py +12 -7
  216. pyglet/text/document.py +19 -17
  217. pyglet/text/formats/html.py +2 -2
  218. pyglet/text/formats/structured.py +10 -40
  219. pyglet/text/layout/__init__.py +20 -13
  220. pyglet/text/layout/base.py +176 -287
  221. pyglet/text/layout/incremental.py +9 -10
  222. pyglet/text/layout/scrolling.py +7 -95
  223. pyglet/window/__init__.py +183 -172
  224. pyglet/window/cocoa/__init__.py +62 -51
  225. pyglet/window/cocoa/pyglet_delegate.py +2 -25
  226. pyglet/window/cocoa/pyglet_view.py +9 -8
  227. pyglet/window/dialog/__init__.py +184 -0
  228. pyglet/window/dialog/base.py +99 -0
  229. pyglet/window/dialog/darwin.py +121 -0
  230. pyglet/window/dialog/linux.py +72 -0
  231. pyglet/window/dialog/windows.py +194 -0
  232. pyglet/window/emscripten/__init__.py +779 -0
  233. pyglet/window/headless/__init__.py +44 -28
  234. pyglet/window/key.py +2 -0
  235. pyglet/window/mouse.py +2 -2
  236. pyglet/window/wayland/__init__.py +377 -0
  237. pyglet/window/win32/__init__.py +101 -46
  238. pyglet/window/xlib/__init__.py +104 -66
  239. {pyglet-2.1.12.dist-info → pyglet-3.0.dev1.dist-info}/METADATA +2 -3
  240. pyglet-3.0.dev1.dist-info/RECORD +322 -0
  241. {pyglet-2.1.12.dist-info → pyglet-3.0.dev1.dist-info}/WHEEL +1 -1
  242. pyglet/gl/__init__.py +0 -208
  243. pyglet/gl/base.py +0 -499
  244. pyglet/gl/cocoa.py +0 -309
  245. pyglet/gl/gl.py +0 -4625
  246. pyglet/gl/gl.pyi +0 -2320
  247. pyglet/gl/gl_compat.pyi +0 -3097
  248. pyglet/gl/gl_info.py +0 -190
  249. pyglet/gl/headless.py +0 -166
  250. pyglet/gl/wgl_info.py +0 -36
  251. pyglet/gl/wglext_nv.py +0 -1096
  252. pyglet/gl/win32.py +0 -268
  253. pyglet/gl/xlib.py +0 -295
  254. pyglet/image/buffer.py +0 -274
  255. pyglet/image/codecs/s3tc.py +0 -354
  256. pyglet/libs/x11/xrandr.py +0 -166
  257. pyglet-2.1.12.dist-info/RECORD +0 -234
  258. /pyglet/{libs/wayland → graphics/api/gl/cocoa}/__init__.py +0 -0
  259. /pyglet/libs/{egl → linux/egl}/egl.py +0 -0
  260. /pyglet/libs/{egl → linux/egl}/lib.py +0 -0
  261. /pyglet/libs/{ioctl.py → linux/ioctl.py} +0 -0
  262. /pyglet/libs/{wayland → linux/wayland}/gbm.py +0 -0
  263. /pyglet/libs/{x11 → linux/x11}/__init__.py +0 -0
  264. /pyglet/libs/{x11 → linux/x11}/cursorfont.py +0 -0
  265. /pyglet/libs/{x11 → linux/x11}/xlib.py +0 -0
  266. /pyglet/libs/{x11 → linux/x11}/xsync.py +0 -0
  267. {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
+