1# ---------------------------------------------------------------------------- 2# pyglet 3# Copyright (c) 2006-2008 Alex Holkner 4# Copyright (c) 2008-2020 pyglet contributors 5# All rights reserved. 6# 7# Redistribution and use in source and binary forms, with or without 8# modification, are permitted provided that the following conditions 9# are met: 10# 11# * Redistributions of source code must retain the above copyright 12# notice, this list of conditions and the following disclaimer. 13# * Redistributions in binary form must reproduce the above copyright 14# notice, this list of conditions and the following disclaimer in 15# the documentation and/or other materials provided with the 16# distribution. 17# * Neither the name of pyglet nor the names of its 18# contributors may be used to endorse or promote products 19# derived from this software without specific prior written 20# permission. 21# 22# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33# POSSIBILITY OF SUCH DAMAGE. 34# ---------------------------------------------------------------------------- 35 36"""Image load, capture and high-level texture functions. 37 38Only basic functionality is described here; for full reference see the 39accompanying documentation. 40 41To load an image:: 42 43 from pyglet import image 44 pic = image.load('picture.png') 45 46The supported image file types include PNG, BMP, GIF, JPG, and many more, 47somewhat depending on the operating system. To load an image from a file-like 48object instead of a filename:: 49 50 pic = image.load('hint.jpg', file=fileobj) 51 52The hint helps the module locate an appropriate decoder to use based on the 53file extension. It is optional. 54 55Once loaded, images can be used directly by most other modules of pyglet. All 56images have a width and height you can access:: 57 58 width, height = pic.width, pic.height 59 60You can extract a region of an image (this keeps the original image intact; 61the memory is shared efficiently):: 62 63 subimage = pic.get_region(x, y, width, height) 64 65Remember that y-coordinates are always increasing upwards. 66 67Drawing images 68-------------- 69 70To draw an image at some point on the screen:: 71 72 pic.blit(x, y, z) 73 74This assumes an appropriate view transform and projection have been applied. 75 76Some images have an intrinsic "anchor point": this is the point which will be 77aligned to the ``x`` and ``y`` coordinates when the image is drawn. By 78default the anchor point is the lower-left corner of the image. You can use 79the anchor point to center an image at a given point, for example:: 80 81 pic.anchor_x = pic.width // 2 82 pic.anchor_y = pic.height // 2 83 pic.blit(x, y, z) 84 85Texture access 86-------------- 87 88If you are using OpenGL directly, you can access the image as a texture:: 89 90 texture = pic.get_texture() 91 92(This is the most efficient way to obtain a texture; some images are 93immediately loaded as textures, whereas others go through an intermediate 94form). To use a texture with pyglet.gl:: 95 96 from pyglet.gl import * 97 glEnable(texture.target) # typically target is GL_TEXTURE_2D 98 glBindTexture(texture.target, texture.id) 99 # ... draw with the texture 100 101Pixel access 102------------ 103 104To access raw pixel data of an image:: 105 106 rawimage = pic.get_image_data() 107 108(If the image has just been loaded this will be a very quick operation; 109however if the image is a texture a relatively expensive readback operation 110will occur). The pixels can be accessed as a string:: 111 112 format = 'RGBA' 113 pitch = rawimage.width * len(format) 114 pixels = rawimage.get_data(format, pitch) 115 116"format" strings consist of characters that give the byte order of each color 117component. For example, if rawimage.format is 'RGBA', there are four color 118components: red, green, blue and alpha, in that order. Other common format 119strings are 'RGB', 'LA' (luminance, alpha) and 'I' (intensity). 120 121The "pitch" of an image is the number of bytes in a row (this may validly be 122more than the number required to make up the width of the image, it is common 123to see this for word alignment). If "pitch" is negative the rows of the image 124are ordered from top to bottom, otherwise they are ordered from bottom to top. 125 126Retrieving data with the format and pitch given in `ImageData.format` and 127`ImageData.pitch` avoids the need for data conversion (assuming you can make 128use of the data in this arbitrary format). 129 130""" 131from io import open, BytesIO 132import re 133import weakref 134 135from ctypes import * 136 137from pyglet.gl import * 138from pyglet.gl import gl_info 139from pyglet.window import * 140from pyglet.util import asbytes 141 142from .codecs import ImageEncodeException, ImageDecodeException 143from .codecs import add_default_image_codecs, add_decoders, add_encoders 144from .codecs import get_animation_decoders, get_decoders, get_encoders 145from .animation import Animation, AnimationFrame 146from . import atlas 147 148 149class ImageException(Exception): 150 pass 151 152 153def load(filename, file=None, decoder=None): 154 """Load an image from a file. 155 156 :note: You can make no assumptions about the return type; usually it will 157 be ImageData or CompressedImageData, but decoders are free to return 158 any subclass of AbstractImage. 159 160 :Parameters: 161 `filename` : str 162 Used to guess the image format, and to load the file if `file` is 163 unspecified. 164 `file` : file-like object or None 165 Source of image data in any supported format. 166 `decoder` : ImageDecoder or None 167 If unspecified, all decoders that are registered for the filename 168 extension are tried. If none succeed, the exception from the 169 first decoder is raised. 170 171 :rtype: AbstractImage 172 """ 173 174 if not file: 175 file = open(filename, 'rb') 176 opened_file = file 177 else: 178 opened_file = None 179 180 if not hasattr(file, 'seek'): 181 file = BytesIO(file.read()) 182 183 try: 184 if decoder: 185 return decoder.decode(file, filename) 186 else: 187 first_exception = None 188 for decoder in get_decoders(filename): 189 try: 190 image = decoder.decode(file, filename) 191 return image 192 except ImageDecodeException as e: 193 if (not first_exception or 194 first_exception.exception_priority < e.exception_priority): 195 first_exception = e 196 file.seek(0) 197 198 if not first_exception: 199 raise ImageDecodeException('No image decoders are available') 200 raise first_exception 201 finally: 202 if opened_file: 203 opened_file.close() 204 205 206def load_animation(filename, file=None, decoder=None): 207 """Load an animation from a file. 208 209 Currently, the only supported format is GIF. 210 211 :Parameters: 212 `filename` : str 213 Used to guess the animation format, and to load the file if `file` 214 is unspecified. 215 `file` : file-like object or None 216 File object containing the animation stream. 217 `decoder` : ImageDecoder or None 218 If unspecified, all decoders that are registered for the filename 219 extension are tried. If none succeed, the exception from the 220 first decoder is raised. 221 222 :rtype: Animation 223 """ 224 if not file: 225 file = open(filename, 'rb') 226 if not hasattr(file, 'seek'): 227 file = BytesIO(file.read()) 228 229 if decoder: 230 return decoder.decode_animation(file, filename) 231 else: 232 first_exception = None 233 for decoder in get_animation_decoders(filename): 234 try: 235 image = decoder.decode_animation(file, filename) 236 return image 237 except ImageDecodeException as e: 238 first_exception = first_exception or e 239 file.seek(0) 240 241 if not first_exception: 242 raise ImageDecodeException('No image decoders are available') 243 raise first_exception 244 245 246def create(width, height, pattern=None): 247 """Create an image optionally filled with the given pattern. 248 249 :note: You can make no assumptions about the return type; usually it will 250 be ImageData or CompressedImageData, but patterns are free to return 251 any subclass of AbstractImage. 252 253 :Parameters: 254 `width` : int 255 Width of image to create 256 `height` : int 257 Height of image to create 258 `pattern` : ImagePattern or None 259 Pattern to fill image with. If unspecified, the image will 260 initially be transparent. 261 262 :rtype: AbstractImage 263 """ 264 if not pattern: 265 pattern = SolidColorImagePattern() 266 return pattern.create_image(width, height) 267 268 269def get_max_texture_size(): 270 """Query the maximum texture size available""" 271 size = c_int() 272 glGetIntegerv(GL_MAX_TEXTURE_SIZE, size) 273 return size.value 274 275 276def _color_as_bytes(color): 277 if sys.version.startswith('2'): 278 return '%c%c%c%c' % color 279 else: 280 if len(color) != 4: 281 raise TypeError("color is expected to have 4 components") 282 return bytes(color) 283 284 285def _nearest_pow2(v): 286 # From http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 287 # Credit: Sean Anderson 288 v -= 1 289 v |= v >> 1 290 v |= v >> 2 291 v |= v >> 4 292 v |= v >> 8 293 v |= v >> 16 294 return v + 1 295 296 297def _is_pow2(v): 298 # http://graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2 299 return (v & (v - 1)) == 0 300 301 302class ImagePattern: 303 """Abstract image creation class.""" 304 305 def create_image(self, width, height): 306 """Create an image of the given size. 307 308 :Parameters: 309 `width` : int 310 Width of image to create 311 `height` : int 312 Height of image to create 313 314 :rtype: AbstractImage 315 """ 316 raise NotImplementedError('abstract') 317 318 319class SolidColorImagePattern(ImagePattern): 320 """Creates an image filled with a solid color.""" 321 322 def __init__(self, color=(0, 0, 0, 0)): 323 """Create a solid image pattern with the given color. 324 325 :Parameters: 326 `color` : (int, int, int, int) 327 4-tuple of ints in range [0,255] giving RGBA components of 328 color to fill with. 329 330 """ 331 self.color = _color_as_bytes(color) 332 333 def create_image(self, width, height): 334 data = self.color * width * height 335 return ImageData(width, height, 'RGBA', data) 336 337 338class CheckerImagePattern(ImagePattern): 339 """Create an image with a tileable checker image. 340 """ 341 342 def __init__(self, color1=(150, 150, 150, 255), color2=(200, 200, 200, 255)): 343 """Initialise with the given colors. 344 345 :Parameters: 346 `color1` : (int, int, int, int) 347 4-tuple of ints in range [0,255] giving RGBA components of 348 color to fill with. This color appears in the top-left and 349 bottom-right corners of the image. 350 `color2` : (int, int, int, int) 351 4-tuple of ints in range [0,255] giving RGBA components of 352 color to fill with. This color appears in the top-right and 353 bottom-left corners of the image. 354 355 """ 356 self.color1 = _color_as_bytes(color1) 357 self.color2 = _color_as_bytes(color2) 358 359 def create_image(self, width, height): 360 hw = width // 2 361 hh = height // 2 362 row1 = self.color1 * hw + self.color2 * hw 363 row2 = self.color2 * hw + self.color1 * hw 364 data = row1 * hh + row2 * hh 365 return ImageData(width, height, 'RGBA', data) 366 367 368class AbstractImage: 369 """Abstract class representing an image. 370 371 :Parameters: 372 `width` : int 373 Width of image 374 `height` : int 375 Height of image 376 `anchor_x` : int 377 X coordinate of anchor, relative to left edge of image data 378 `anchor_y` : int 379 Y coordinate of anchor, relative to bottom edge of image data 380 """ 381 anchor_x = 0 382 anchor_y = 0 383 384 _is_rectangle = False 385 386 def __init__(self, width, height): 387 self.width = width 388 self.height = height 389 390 def __repr__(self): 391 return '<%s %dx%d>' % (self.__class__.__name__, self.width, self.height) 392 393 def get_image_data(self): 394 """Get an ImageData view of this image. 395 396 Changes to the returned instance may or may not be reflected in this 397 image. 398 399 :rtype: :py:class:`~pyglet.image.ImageData` 400 401 .. versionadded:: 1.1 402 """ 403 raise ImageException('Cannot retrieve image data for %r' % self) 404 405 def get_texture(self, rectangle=False, force_rectangle=False): 406 """A :py:class:`~pyglet.image.Texture` view of this image. 407 408 By default, textures are created with dimensions that are powers of 409 two. Smaller images will return a :py:class:`~pyglet.image.TextureRegion` that covers just 410 the image portion of the larger texture. This restriction is required 411 on older video cards, and for compressed textures, or where texture 412 repeat modes will be used, or where mipmapping is desired. 413 414 If the `rectangle` parameter is ``True``, this restriction is ignored 415 and a texture the size of the image may be created if the driver 416 supports the ``GL_ARB_texture_rectangle`` or 417 ``GL_NV_texture_rectangle`` extensions. If the extensions are not 418 present, the image already is a texture, or the image has power 2 419 dimensions, the `rectangle` parameter is ignored. 420 421 Examine `Texture.target` to determine if the returned texture is a 422 rectangle (``GL_TEXTURE_RECTANGLE_ARB`` or 423 ``GL_TEXTURE_RECTANGLE_NV``) or not (``GL_TEXTURE_2D``). 424 425 If the `force_rectangle` parameter is ``True``, one of these 426 extensions must be present, and the returned texture always 427 has target ``GL_TEXTURE_RECTANGLE_ARB`` or ``GL_TEXTURE_RECTANGLE_NV``. 428 429 Changes to the returned instance may or may not be reflected in this 430 image. 431 432 :Parameters: 433 `rectangle` : bool 434 True if the texture can be created as a rectangle. 435 `force_rectangle` : bool 436 True if the texture must be created as a rectangle. 437 438 .. versionadded:: 1.1.4. 439 :rtype: :py:class:`~pyglet.image.Texture` 440 441 .. versionadded:: 1.1 442 """ 443 raise ImageException('Cannot retrieve texture for %r' % self) 444 445 def get_mipmapped_texture(self): 446 """Retrieve a :py:class:`~pyglet.image.Texture` instance with all mipmap levels filled in. 447 448 Requires that image dimensions be powers of 2. 449 450 :rtype: :py:class:`~pyglet.image.Texture` 451 452 .. versionadded:: 1.1 453 """ 454 raise ImageException('Cannot retrieve mipmapped texture for %r' % self) 455 456 def get_region(self, x, y, width, height): 457 """Retrieve a rectangular region of this image. 458 459 :Parameters: 460 `x` : int 461 Left edge of region. 462 `y` : int 463 Bottom edge of region. 464 `width` : int 465 Width of region. 466 `height` : int 467 Height of region. 468 469 :rtype: AbstractImage 470 """ 471 raise ImageException('Cannot get region for %r' % self) 472 473 def save(self, filename=None, file=None, encoder=None): 474 """Save this image to a file. 475 476 :Parameters: 477 `filename` : str 478 Used to set the image file format, and to open the output file 479 if `file` is unspecified. 480 `file` : file-like object or None 481 File to write image data to. 482 `encoder` : ImageEncoder or None 483 If unspecified, all encoders matching the filename extension 484 are tried. If all fail, the exception from the first one 485 attempted is raised. 486 487 """ 488 if not file: 489 file = open(filename, 'wb') 490 491 if encoder: 492 encoder.encode(self, file, filename) 493 else: 494 first_exception = None 495 for encoder in get_encoders(filename): 496 try: 497 encoder.encode(self, file, filename) 498 return 499 except ImageEncodeException as e: 500 first_exception = first_exception or e 501 file.seek(0) 502 503 if not first_exception: 504 raise ImageEncodeException( 505 'No image encoders are available') 506 raise first_exception 507 508 def blit(self, x, y, z=0): 509 """Draw this image to the active framebuffers. 510 511 The image will be drawn with the lower-left corner at 512 (``x -`` `anchor_x`, ``y -`` `anchor_y`, ``z``). 513 """ 514 raise ImageException('Cannot blit %r.' % self) 515 516 def blit_into(self, source, x, y, z): 517 """Draw `source` on this image. 518 519 `source` will be copied into this image such that its anchor point 520 is aligned with the `x` and `y` parameters. If this image is a 3D 521 texture, the `z` coordinate gives the image slice to copy into. 522 523 Note that if `source` is larger than this image (or the positioning 524 would cause the copy to go out of bounds) then you must pass a 525 region of `source` to this method, typically using get_region(). 526 """ 527 raise ImageException('Cannot blit images onto %r.' % self) 528 529 def blit_to_texture(self, target, level, x, y, z=0): 530 """Draw this image on the currently bound texture at `target`. 531 532 This image is copied into the texture such that this image's anchor 533 point is aligned with the given `x` and `y` coordinates of the 534 destination texture. If the currently bound texture is a 3D texture, 535 the `z` coordinate gives the image slice to blit into. 536 """ 537 raise ImageException('Cannot blit %r to a texture.' % self) 538 539 540class AbstractImageSequence: 541 """Abstract sequence of images. 542 543 The sequence is useful for storing image animations or slices of a volume. 544 For efficient access, use the `texture_sequence` member. The class 545 also implements the sequence interface (`__len__`, `__getitem__`, 546 `__setitem__`). 547 """ 548 549 def get_texture_sequence(self): 550 """Get a TextureSequence. 551 552 :rtype: `TextureSequence` 553 554 .. versionadded:: 1.1 555 """ 556 raise NotImplementedError('abstract') 557 558 def get_animation(self, period, loop=True): 559 """Create an animation over this image sequence for the given constant 560 framerate. 561 562 :Parameters 563 `period` : float 564 Number of seconds to display each frame. 565 `loop` : bool 566 If True, the animation will loop continuously. 567 568 :rtype: :py:class:`~pyglet.image.Animation` 569 570 .. versionadded:: 1.1 571 """ 572 return Animation.from_image_sequence(self, period, loop) 573 574 def __getitem__(self, slice): 575 """Retrieve a (list of) image. 576 577 :rtype: AbstractImage 578 """ 579 raise NotImplementedError('abstract') 580 581 def __setitem__(self, slice, image): 582 """Replace one or more images in the sequence. 583 584 :Parameters: 585 `image` : `~pyglet.image.AbstractImage` 586 The replacement image. The actual instance may not be used, 587 depending on this implementation. 588 589 """ 590 raise NotImplementedError('abstract') 591 592 def __len__(self): 593 raise NotImplementedError('abstract') 594 595 def __iter__(self): 596 """Iterate over the images in sequence. 597 598 :rtype: Iterator 599 600 .. versionadded:: 1.1 601 """ 602 raise NotImplementedError('abstract') 603 604 605class TextureSequence(AbstractImageSequence): 606 """Interface for a sequence of textures. 607 608 Typical implementations store multiple :py:class:`~pyglet.image.TextureRegion` s within one 609 :py:class:`~pyglet.image.Texture` so as to minimise state changes. 610 """ 611 612 def get_texture_sequence(self): 613 return self 614 615 616class UniformTextureSequence(TextureSequence): 617 """Interface for a sequence of textures, each with the same dimensions. 618 619 :Parameters: 620 `item_width` : int 621 Width of each texture in the sequence. 622 `item_height` : int 623 Height of each texture in the sequence. 624 625 """ 626 627 def _get_item_width(self): 628 raise NotImplementedError('abstract') 629 630 def _get_item_height(self): 631 raise NotImplementedError('abstract') 632 633 @property 634 def item_width(self): 635 return self._get_item_width() 636 637 @property 638 def item_height(self): 639 return self._get_item_height() 640 641 642class ImageData(AbstractImage): 643 """An image represented as a string of unsigned bytes. 644 645 :Parameters: 646 `data` : str 647 Pixel data, encoded according to `format` and `pitch`. 648 `format` : str 649 The format string to use when reading or writing `data`. 650 `pitch` : int 651 Number of bytes per row. Negative values indicate a top-to-bottom 652 arrangement. 653 654 """ 655 _swap1_pattern = re.compile(asbytes('(.)'), re.DOTALL) 656 _swap2_pattern = re.compile(asbytes('(.)(.)'), re.DOTALL) 657 _swap3_pattern = re.compile(asbytes('(.)(.)(.)'), re.DOTALL) 658 _swap4_pattern = re.compile(asbytes('(.)(.)(.)(.)'), re.DOTALL) 659 660 _current_texture = None 661 _current_mipmap_texture = None 662 663 def __init__(self, width, height, format, data, pitch=None): 664 """Initialise image data. 665 666 :Parameters: 667 `width` : int 668 Width of image data 669 `height` : int 670 Height of image data 671 `format` : str 672 A valid format string, such as 'RGB', 'RGBA', 'ARGB', etc. 673 `data` : sequence 674 String or array/list of bytes giving the decoded data. 675 `pitch` : int or None 676 If specified, the number of bytes per row. Negative values 677 indicate a top-to-bottom arrangement. Defaults to 678 ``width * len(format)``. 679 680 """ 681 super(ImageData, self).__init__(width, height) 682 683 self._current_format = self._desired_format = format.upper() 684 self._current_data = data 685 if not pitch: 686 pitch = width * len(format) 687 self._current_pitch = self.pitch = pitch 688 self.mipmap_images = [] 689 690 def __getstate__(self): 691 return { 692 'width': self.width, 693 'height': self.height, 694 '_current_data': self.get_data(self._current_format, self._current_pitch), 695 '_current_format': self._current_format, 696 '_desired_format': self._desired_format, 697 '_current_pitch': self._current_pitch, 698 'pitch': self.pitch, 699 'mipmap_images': self.mipmap_images 700 } 701 702 def get_image_data(self): 703 return self 704 705 @property 706 def format(self): 707 """Format string of the data. Read-write. 708 709 :type: str 710 """ 711 return self._desired_format 712 713 @format.setter 714 def format(self, fmt): 715 self._desired_format = fmt.upper() 716 self._current_texture = None 717 718 def get_data(self, fmt=None, pitch=None): 719 """Get the byte data of the image. 720 721 :Parameters: 722 `fmt` : str 723 Format string of the return data. 724 `pitch` : int 725 Number of bytes per row. Negative values indicate a 726 top-to-bottom arrangement. 727 728 .. versionadded:: 1.1 729 730 :rtype: sequence of bytes, or str 731 """ 732 fmt = fmt or self._desired_format 733 pitch = pitch or self._current_pitch 734 735 if fmt == self._current_format and pitch == self._current_pitch: 736 return self._current_data 737 return self._convert(fmt, pitch) 738 739 def set_data(self, fmt, pitch, data): 740 """Set the byte data of the image. 741 742 :Parameters: 743 `fmt` : str 744 Format string of the return data. 745 `pitch` : int 746 Number of bytes per row. Negative values indicate a 747 top-to-bottom arrangement. 748 `data` : str or sequence of bytes 749 Image data. 750 751 .. versionadded:: 1.1 752 """ 753 self._current_format = fmt 754 self._current_pitch = pitch 755 self._current_data = data 756 self._current_texture = None 757 self._current_mipmap_texture = None 758 759 def set_mipmap_image(self, level, image): 760 """Set a mipmap image for a particular level. 761 762 The mipmap image will be applied to textures obtained via 763 `get_mipmapped_texture`. 764 765 :Parameters: 766 `level` : int 767 Mipmap level to set image at, must be >= 1. 768 `image` : AbstractImage 769 Image to set. Must have correct dimensions for that mipmap 770 level (i.e., width >> level, height >> level) 771 """ 772 773 if level == 0: 774 raise ImageException( 775 'Cannot set mipmap image at level 0 (it is this image)') 776 777 if not _is_pow2(self.width) or not _is_pow2(self.height): 778 raise ImageException( 779 'Image dimensions must be powers of 2 to use mipmaps.') 780 781 # Check dimensions of mipmap 782 width, height = self.width, self.height 783 for i in range(level): 784 width >>= 1 785 height >>= 1 786 if width != image.width or height != image.height: 787 raise ImageException( 788 'Mipmap image has wrong dimensions for level %d' % level) 789 790 # Extend mipmap_images list to required level 791 self.mipmap_images += [None] * (level - len(self.mipmap_images)) 792 self.mipmap_images[level - 1] = image 793 794 def create_texture(self, cls, rectangle=False, force_rectangle=False): 795 """Create a texture containing this image. 796 797 If the image's dimensions are not powers of 2, a TextureRegion of 798 a larger Texture will be returned that matches the dimensions of this 799 image. 800 801 :Parameters: 802 `cls` : class (subclass of Texture) 803 Class to construct. 804 `rectangle` : bool 805 ``True`` if a rectangle can be created; see 806 `AbstractImage.get_texture`. 807 808 .. versionadded:: 1.1 809 `force_rectangle` : bool 810 ``True`` if a rectangle must be created; see 811 `AbstractImage.get_texture`. 812 813 .. versionadded:: 1.1.4 814 815 :rtype: cls or cls.region_class 816 """ 817 internalformat = self._get_internalformat(self.format) 818 texture = cls.create(self.width, self.height, internalformat, 819 rectangle, force_rectangle) 820 if self.anchor_x or self.anchor_y: 821 texture.anchor_x = self.anchor_x 822 texture.anchor_y = self.anchor_y 823 824 self.blit_to_texture(texture.target, texture.level, 825 self.anchor_x, self.anchor_y, 0, None) 826 827 return texture 828 829 def get_texture(self, rectangle=False, force_rectangle=False): 830 if (not self._current_texture or 831 (not self._current_texture._is_rectangle and force_rectangle)): 832 self._current_texture = self.create_texture(Texture, rectangle, force_rectangle) 833 return self._current_texture 834 835 def get_mipmapped_texture(self): 836 """Return a Texture with mipmaps. 837 838 If :py:class:`~pyglet.image.set_mipmap_Image` has been called with at least one image, the set 839 of images defined will be used. Otherwise, mipmaps will be 840 automatically generated. 841 842 The texture dimensions must be powers of 2 to use mipmaps. 843 844 :rtype: :py:class:`~pyglet.image.Texture` 845 846 .. versionadded:: 1.1 847 """ 848 if self._current_mipmap_texture: 849 return self._current_mipmap_texture 850 851 if not _is_pow2(self.width) or not _is_pow2(self.height): 852 raise ImageException( 853 'Image dimensions must be powers of 2 to use mipmaps.') 854 855 texture = Texture.create_for_size(GL_TEXTURE_2D, self.width, self.height) 856 if self.anchor_x or self.anchor_y: 857 texture.anchor_x = self.anchor_x 858 texture.anchor_y = self.anchor_y 859 860 internalformat = self._get_internalformat(self.format) 861 862 glBindTexture(texture.target, texture.id) 863 glTexParameteri(texture.target, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) 864 865 if self.mipmap_images: 866 self.blit_to_texture(texture.target, texture.level, 867 self.anchor_x, self.anchor_y, 0, internalformat) 868 level = 0 869 for image in self.mipmap_images: 870 level += 1 871 if image: 872 image.blit_to_texture(texture.target, level, 873 self.anchor_x, self.anchor_y, 0, internalformat) 874 # TODO: should set base and max mipmap level if some mipmaps are missing. 875 else: 876 glTexParameteri(texture.target, GL_GENERATE_MIPMAP, GL_TRUE) 877 self.blit_to_texture(texture.target, texture.level, 878 self.anchor_x, self.anchor_y, 0, internalformat) 879 880 self._current_mipmap_texture = texture 881 return texture 882 883 def get_region(self, x, y, width, height): 884 """Retrieve a rectangular region of this image data. 885 886 :Parameters: 887 `x` : int 888 Left edge of region. 889 `y` : int 890 Bottom edge of region. 891 `width` : int 892 Width of region. 893 `height` : int 894 Height of region. 895 896 :rtype: ImageDataRegion 897 """ 898 return ImageDataRegion(x, y, width, height, self) 899 900 def blit(self, x, y, z=0, width=None, height=None): 901 self.get_texture().blit(x, y, z, width, height) 902 903 def blit_to_texture(self, target, level, x, y, z, internalformat=None): 904 """Draw this image to to the currently bound texture at `target`. 905 906 This image's anchor point will be aligned to the given `x` and `y` 907 coordinates. If the currently bound texture is a 3D texture, the `z` 908 parameter gives the image slice to blit into. 909 910 If `internalformat` is specified, glTexImage is used to initialise 911 the texture; otherwise, glTexSubImage is used to update a region. 912 """ 913 x -= self.anchor_x 914 y -= self.anchor_y 915 916 data_format = self.format 917 data_pitch = abs(self._current_pitch) 918 919 # Determine pixel format from format string 920 matrix = None 921 format, type = self._get_gl_format_and_type(data_format) 922 if format is None: 923 if (len(data_format) in (3, 4) and 924 gl_info.have_extension('GL_ARB_imaging')): 925 # Construct a color matrix to convert to GL_RGBA 926 def component_column(component): 927 try: 928 pos = 'RGBA'.index(component) 929 return [0] * pos + [1] + [0] * (3 - pos) 930 except ValueError: 931 return [0, 0, 0, 0] 932 933 # pad to avoid index exceptions 934 lookup_format = data_format + 'XXX' 935 matrix = (component_column(lookup_format[0]) + 936 component_column(lookup_format[1]) + 937 component_column(lookup_format[2]) + 938 component_column(lookup_format[3])) 939 format = { 940 3: GL_RGB, 941 4: GL_RGBA}.get(len(data_format)) 942 type = GL_UNSIGNED_BYTE 943 944 glMatrixMode(GL_COLOR) 945 glPushMatrix() 946 glLoadMatrixf((GLfloat * 16)(*matrix)) 947 else: 948 # Need to convert data to a standard form 949 data_format = { 950 1: 'L', 951 2: 'LA', 952 3: 'RGB', 953 4: 'RGBA'}.get(len(data_format)) 954 format, type = self._get_gl_format_and_type(data_format) 955 956 # Workaround: don't use GL_UNPACK_ROW_LENGTH 957 if gl.current_context._workaround_unpack_row_length: 958 data_pitch = self.width * len(data_format) 959 960 # Get data in required format (hopefully will be the same format it's 961 # already in, unless that's an obscure format, upside-down or the 962 # driver is old). 963 data = self._convert(data_format, data_pitch) 964 965 if data_pitch & 0x1: 966 alignment = 1 967 elif data_pitch & 0x2: 968 alignment = 2 969 else: 970 alignment = 4 971 row_length = data_pitch // len(data_format) 972 glPushClientAttrib(GL_CLIENT_PIXEL_STORE_BIT) 973 glPixelStorei(GL_UNPACK_ALIGNMENT, alignment) 974 glPixelStorei(GL_UNPACK_ROW_LENGTH, row_length) 975 self._apply_region_unpack() 976 977 if target == GL_TEXTURE_3D: 978 assert not internalformat 979 glTexSubImage3D(target, level, 980 x, y, z, 981 self.width, self.height, 1, 982 format, type, 983 data) 984 elif internalformat: 985 glTexImage2D(target, level, 986 internalformat, 987 self.width, self.height, 988 0, 989 format, type, 990 data) 991 else: 992 glTexSubImage2D(target, level, 993 x, y, 994 self.width, self.height, 995 format, type, 996 data) 997 glPopClientAttrib() 998 999 if matrix: 1000 glPopMatrix() 1001 glMatrixMode(GL_MODELVIEW) 1002 1003 # Flush image upload before data get GC'd. 1004 glFlush() 1005 1006 def _apply_region_unpack(self): 1007 pass 1008 1009 def _convert(self, format, pitch): 1010 """Return data in the desired format; does not alter this instance's 1011 current format or pitch. 1012 """ 1013 if format == self._current_format and pitch == self._current_pitch: 1014 if type(self._current_data) is str: 1015 return asbytes(self._current_data) 1016 return self._current_data 1017 1018 self._ensure_string_data() 1019 data = self._current_data 1020 current_pitch = self._current_pitch 1021 current_format = self._current_format 1022 sign_pitch = current_pitch // abs(current_pitch) 1023 if format != self._current_format: 1024 # Create replacement string, e.g. r'\4\1\2\3' to convert RGBA to 1025 # ARGB 1026 repl = asbytes('') 1027 for c in format: 1028 try: 1029 idx = current_format.index(c) + 1 1030 except ValueError: 1031 idx = 1 1032 repl += asbytes(r'\%d' % idx) 1033 1034 if len(current_format) == 1: 1035 swap_pattern = self._swap1_pattern 1036 elif len(current_format) == 2: 1037 swap_pattern = self._swap2_pattern 1038 elif len(current_format) == 3: 1039 swap_pattern = self._swap3_pattern 1040 elif len(current_format) == 4: 1041 swap_pattern = self._swap4_pattern 1042 else: 1043 raise ImageException( 1044 'Current image format is wider than 32 bits.') 1045 1046 packed_pitch = self.width * len(current_format) 1047 if abs(self._current_pitch) != packed_pitch: 1048 # Pitch is wider than pixel data, need to go row-by-row. 1049 new_pitch = abs(self._current_pitch) 1050 rows = [data[i:i+new_pitch] for i in range(0, len(data), new_pitch)] 1051 rows = [swap_pattern.sub(repl, r[:packed_pitch]) for r in rows] 1052 data = asbytes('').join(rows) 1053 else: 1054 # Rows are tightly packed, apply regex over whole image. 1055 data = swap_pattern.sub(repl, data) 1056 1057 # After conversion, rows will always be tightly packed 1058 current_pitch = sign_pitch * (len(format) * self.width) 1059 1060 if pitch != current_pitch: 1061 diff = abs(current_pitch) - abs(pitch) 1062 if diff > 0: 1063 # New pitch is shorter than old pitch, chop bytes off each row 1064 new_pitch = abs(pitch) 1065 rows = [data[i:i+new_pitch-diff] for i in range(0, len(data), new_pitch)] 1066 elif diff < 0: 1067 # New pitch is longer than old pitch, add '0' bytes to each row 1068 new_pitch = abs(current_pitch) 1069 padding = asbytes(1) * -diff 1070 rows = [data[i:i+new_pitch] + padding for i in range(0, len(data), new_pitch)] 1071 1072 if current_pitch * pitch < 0: 1073 # Pitch differs in sign, swap row order 1074 new_pitch = abs(pitch) 1075 rows = [data[i:i+new_pitch] for i in range(0, len(data), new_pitch)] 1076 rows.reverse() 1077 1078 data = asbytes('').join(rows) 1079 1080 return asbytes(data) 1081 1082 def _ensure_string_data(self): 1083 if type(self._current_data) is not bytes: 1084 buf = create_string_buffer(len(self._current_data)) 1085 memmove(buf, self._current_data, len(self._current_data)) 1086 self._current_data = buf.raw 1087 1088 def _get_gl_format_and_type(self, format): 1089 if format == 'I': 1090 return GL_LUMINANCE, GL_UNSIGNED_BYTE 1091 elif format == 'L': 1092 return GL_LUMINANCE, GL_UNSIGNED_BYTE 1093 elif format == 'LA': 1094 return GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE 1095 elif format == 'R': 1096 return GL_RED, GL_UNSIGNED_BYTE 1097 elif format == 'G': 1098 return GL_GREEN, GL_UNSIGNED_BYTE 1099 elif format == 'B': 1100 return GL_BLUE, GL_UNSIGNED_BYTE 1101 elif format == 'A': 1102 return GL_ALPHA, GL_UNSIGNED_BYTE 1103 elif format == 'RGB': 1104 return GL_RGB, GL_UNSIGNED_BYTE 1105 elif format == 'RGBA': 1106 return GL_RGBA, GL_UNSIGNED_BYTE 1107 elif (format == 'ARGB' and 1108 gl_info.have_extension('GL_EXT_bgra') and 1109 gl_info.have_extension('GL_APPLE_packed_pixels')): 1110 return GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV 1111 elif (format == 'ABGR' and 1112 gl_info.have_extension('GL_EXT_abgr')): 1113 return GL_ABGR_EXT, GL_UNSIGNED_BYTE 1114 elif (format == 'BGR' and 1115 gl_info.have_extension('GL_EXT_bgra')): 1116 return GL_BGR, GL_UNSIGNED_BYTE 1117 elif (format == 'BGRA' and 1118 gl_info.have_extension('GL_EXT_bgra')): 1119 return GL_BGRA, GL_UNSIGNED_BYTE 1120 1121 return None, None 1122 1123 def _get_internalformat(self, format): 1124 if len(format) == 4: 1125 return GL_RGBA 1126 elif len(format) == 3: 1127 return GL_RGB 1128 elif len(format) == 2: 1129 return GL_LUMINANCE_ALPHA 1130 elif format == 'A': 1131 return GL_ALPHA 1132 elif format == 'L': 1133 return GL_LUMINANCE 1134 elif format == 'I': 1135 return GL_INTENSITY 1136 return GL_RGBA 1137 1138 1139class ImageDataRegion(ImageData): 1140 def __init__(self, x, y, width, height, image_data): 1141 super(ImageDataRegion, self).__init__(width, height, 1142 image_data._current_format, image_data._current_data, 1143 image_data._current_pitch) 1144 self.x = x 1145 self.y = y 1146 1147 def __getstate__(self): 1148 return { 1149 'width': self.width, 1150 'height': self.height, 1151 '_current_data': 1152 self.get_data(self._current_format, self._current_pitch), 1153 '_current_format': self._current_format, 1154 '_desired_format': self._desired_format, 1155 '_current_pitch': self._current_pitch, 1156 'pitch': self.pitch, 1157 'mipmap_images': self.mipmap_images, 1158 'x': self.x, 1159 'y': self.y 1160 } 1161 1162 def get_data(self, fmt=None, pitch=None): 1163 x1 = len(self._current_format) * self.x 1164 x2 = len(self._current_format) * (self.x + self.width) 1165 1166 self._ensure_string_data() 1167 data = self._convert(self._current_format, abs(self._current_pitch)) 1168 new_pitch = abs(self._current_pitch) 1169 rows = [data[i:i+new_pitch] for i in range(0, len(data), new_pitch)] 1170 rows = [row[x1:x2] for row in rows[self.y:self.y + self.height]] 1171 self._current_data = b''.join(rows) 1172 self._current_pitch = self.width * len(self._current_format) 1173 self._current_texture = None 1174 self.x = 0 1175 self.y = 0 1176 1177 fmt = fmt or self._desired_format 1178 pitch = pitch or self._current_pitch 1179 return super(ImageDataRegion, self).get_data(fmt, pitch) 1180 1181 def set_data(self, fmt, pitch, data): 1182 self.x = 0 1183 self.y = 0 1184 super(ImageDataRegion, self).set_data(fmt, pitch, data) 1185 1186 def _apply_region_unpack(self): 1187 glPixelStorei(GL_UNPACK_SKIP_PIXELS, self.x) 1188 glPixelStorei(GL_UNPACK_SKIP_ROWS, self.y) 1189 1190 def get_region(self, x, y, width, height): 1191 x += self.x 1192 y += self.y 1193 return super(ImageDataRegion, self).get_region(x, y, width, height) 1194 1195 1196class CompressedImageData(AbstractImage): 1197 """Image representing some compressed data suitable for direct uploading 1198 to driver. 1199 """ 1200 1201 _current_texture = None 1202 _current_mipmap_texture = None 1203 1204 def __init__(self, width, height, gl_format, data, extension=None, decoder=None): 1205 """Construct a CompressedImageData with the given compressed data. 1206 1207 :Parameters: 1208 `width` : int 1209 Width of image 1210 `height` : int 1211 Height of image 1212 `gl_format` : int 1213 GL constant giving format of compressed data; for example, 1214 ``GL_COMPRESSED_RGBA_S3TC_DXT5_EXT``. 1215 `data` : sequence 1216 String or array/list of bytes giving compressed image data. 1217 `extension` : str or None 1218 If specified, gives the name of a GL extension to check for 1219 before creating a texture. 1220 `decoder` : function(data, width, height) -> AbstractImage 1221 A function to decode the compressed data, to be used if the 1222 required extension is not present. 1223 1224 """ 1225 super(CompressedImageData, self).__init__(width, height) 1226 self.data = data 1227 self.gl_format = gl_format 1228 self.extension = extension 1229 self.decoder = decoder 1230 self.mipmap_data = [] 1231 1232 def set_mipmap_data(self, level, data): 1233 """Set data for a mipmap level. 1234 1235 Supplied data gives a compressed image for the given mipmap level. 1236 The image must be of the correct dimensions for the level 1237 (i.e., width >> level, height >> level); but this is not checked. If 1238 any mipmap levels are specified, they are used; otherwise, mipmaps for 1239 `mipmapped_texture` are generated automatically. 1240 1241 :Parameters: 1242 `level` : int 1243 Level of mipmap image to set. 1244 `data` : sequence 1245 String or array/list of bytes giving compressed image data. 1246 Data must be in same format as specified in constructor. 1247 1248 """ 1249 # Extend mipmap_data list to required level 1250 self.mipmap_data += [None] * (level - len(self.mipmap_data)) 1251 self.mipmap_data[level - 1] = data 1252 1253 def _have_extension(self): 1254 return self.extension is None or gl_info.have_extension(self.extension) 1255 1256 def _verify_driver_supported(self): 1257 """Assert that the extension required for this image data is 1258 supported. 1259 1260 Raises `ImageException` if not. 1261 """ 1262 1263 if not self._have_extension(): 1264 raise ImageException('%s is required to decode %r' % (self.extension, self)) 1265 1266 def get_texture(self, rectangle=False, force_rectangle=False): 1267 if force_rectangle: 1268 raise ImageException('Compressed texture rectangles not supported') 1269 1270 if self._current_texture: 1271 return self._current_texture 1272 1273 tex_id = GLuint() 1274 glGenTextures(1, byref(tex_id)) 1275 texture = Texture(self.width, self.height, GL_TEXTURE_2D, tex_id.value) 1276 glBindTexture(GL_TEXTURE_2D, tex_id) 1277 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, Texture.default_min_filter) 1278 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, Texture.default_mag_filter) 1279 1280 if self.anchor_x or self.anchor_y: 1281 texture.anchor_x = self.anchor_x 1282 texture.anchor_y = self.anchor_y 1283 1284 if self._have_extension(): 1285 glCompressedTexImage2D(texture.target, texture.level, 1286 self.gl_format, 1287 self.width, self.height, 0, 1288 len(self.data), self.data) 1289 else: 1290 image = self.decoder(self.data, self.width, self.height) 1291 texture = image.get_texture() 1292 assert texture.width == self.width 1293 assert texture.height == self.height 1294 1295 glFlush() 1296 self._current_texture = texture 1297 return texture 1298 1299 def get_mipmapped_texture(self): 1300 if self._current_mipmap_texture: 1301 return self._current_mipmap_texture 1302 1303 if not self._have_extension(): 1304 # TODO mip-mapped software decoded compressed textures. For now, 1305 # just return a non-mipmapped texture. 1306 return self.get_texture() 1307 1308 texture = Texture.create_for_size(GL_TEXTURE_2D, self.width, self.height) 1309 if self.anchor_x or self.anchor_y: 1310 texture.anchor_x = self.anchor_x 1311 texture.anchor_y = self.anchor_y 1312 1313 glBindTexture(texture.target, texture.id) 1314 1315 glTexParameteri(texture.target, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) 1316 1317 if not self.mipmap_data: 1318 glTexParameteri(texture.target, GL_GENERATE_MIPMAP, GL_TRUE) 1319 1320 glCompressedTexImage2DARB(texture.target, texture.level, 1321 self.gl_format, 1322 self.width, self.height, 0, 1323 len(self.data), self.data) 1324 1325 width, height = self.width, self.height 1326 level = 0 1327 for data in self.mipmap_data: 1328 width >>= 1 1329 height >>= 1 1330 level += 1 1331 glCompressedTexImage2DARB(texture.target, level, 1332 self.gl_format, 1333 width, height, 0, 1334 len(data), data) 1335 1336 glFlush() 1337 1338 self._current_mipmap_texture = texture 1339 return texture 1340 1341 def blit_to_texture(self, target, level, x, y, z): 1342 self._verify_driver_supported() 1343 1344 if target == GL_TEXTURE_3D: 1345 glCompressedTexSubImage3DARB(target, level, 1346 x - self.anchor_x, y - self.anchor_y, z, 1347 self.width, self.height, 1, 1348 self.gl_format, 1349 len(self.data), self.data) 1350 else: 1351 glCompressedTexSubImage2DARB(target, level, 1352 x - self.anchor_x, y - self.anchor_y, 1353 self.width, self.height, 1354 self.gl_format, 1355 len(self.data), self.data) 1356 1357 1358class Texture(AbstractImage): 1359 """An image loaded into video memory that can be efficiently drawn 1360 to the framebuffer. 1361 1362 Typically you will get an instance of Texture by accessing the `texture` 1363 member of any other AbstractImage. 1364 1365 :Parameters: 1366 `region_class` : class (subclass of TextureRegion) 1367 Class to use when constructing regions of this texture. 1368 `tex_coords` : tuple 1369 12-tuple of float, named (u1, v1, r1, u2, v2, r2, ...). u, v, r 1370 give the 3D texture coordinates for vertices 1-4. The vertices 1371 are specified in the order bottom-left, bottom-right, top-right 1372 and top-left. 1373 `target` : int 1374 The GL texture target (e.g., ``GL_TEXTURE_2D``). 1375 `level` : int 1376 The mipmap level of this texture. 1377 1378 """ 1379 1380 region_class = None # Set to TextureRegion after it's defined 1381 tex_coords = (0., 0., 0., 1., 0., 0., 1., 1., 0., 0., 1., 0.) 1382 tex_coords_order = (0, 1, 2, 3) 1383 level = 0 1384 images = 1 1385 x = y = z = 0 1386 default_min_filter = GL_LINEAR 1387 default_mag_filter = GL_LINEAR 1388 1389 def __init__(self, width, height, target, id): 1390 super(Texture, self).__init__(width, height) 1391 self.target = target 1392 self.id = id 1393 self._context = gl.current_context 1394 1395 def __del__(self): 1396 try: 1397 self._context.delete_texture(self.id) 1398 except: 1399 pass 1400 1401 @classmethod 1402 def create(cls, width, height, internalformat=GL_RGBA, 1403 rectangle=False, force_rectangle=False, min_filter=None, mag_filter=None): 1404 """Create an empty Texture. 1405 1406 If `rectangle` is ``False`` or the appropriate driver extensions are 1407 not available, a larger texture than requested will be created, and 1408 a :py:class:`~pyglet.image.TextureRegion` corresponding to the requested size will be 1409 returned. 1410 1411 :Parameters: 1412 `width` : int 1413 Width of the texture. 1414 `height` : int 1415 Height of the texture. 1416 `internalformat` : int 1417 GL constant giving the internal format of the texture; for 1418 example, ``GL_RGBA``. 1419 `rectangle` : bool 1420 ``True`` if a rectangular texture is permitted. See 1421 `AbstractImage.get_texture`. 1422 `force_rectangle` : bool 1423 ``True`` if a rectangular texture is required. See 1424 `AbstractImage.get_texture`. 1425 1426 .. versionadded:: 1.1.4. 1427 `min_filter` : int 1428 The minifaction filter used for this texture, commonly ``GL_LINEAR`` or ``GL_NEAREST`` 1429 `mag_filter` : int 1430 The magnification filter used for this texture, commonly ``GL_LINEAR`` or ``GL_NEAREST`` 1431 1432 :rtype: :py:class:`~pyglet.image.Texture` 1433 1434 .. versionadded:: 1.1 1435 """ 1436 min_filter = min_filter or cls.default_min_filter 1437 mag_filter = mag_filter or cls.default_mag_filter 1438 target = GL_TEXTURE_2D 1439 if rectangle or force_rectangle: 1440 if not force_rectangle and _is_pow2(width) and _is_pow2(height): 1441 rectangle = False 1442 elif gl_info.have_extension('GL_ARB_texture_rectangle'): 1443 target = GL_TEXTURE_RECTANGLE_ARB 1444 rectangle = True 1445 elif gl_info.have_extension('GL_NV_texture_rectangle'): 1446 target = GL_TEXTURE_RECTANGLE_NV 1447 rectangle = True 1448 else: 1449 rectangle = False 1450 1451 if force_rectangle and not rectangle: 1452 raise ImageException('Texture rectangle extensions not available') 1453 1454 if rectangle: 1455 texture_width = width 1456 texture_height = height 1457 else: 1458 texture_width = _nearest_pow2(width) 1459 texture_height = _nearest_pow2(height) 1460 1461 id = GLuint() 1462 glGenTextures(1, byref(id)) 1463 glBindTexture(target, id.value) 1464 glTexParameteri(target, GL_TEXTURE_MIN_FILTER, min_filter) 1465 glTexParameteri(target, GL_TEXTURE_MAG_FILTER, mag_filter) 1466 1467 blank = (GLubyte * (texture_width * texture_height * 4))() 1468 glTexImage2D(target, 0, 1469 internalformat, 1470 texture_width, texture_height, 1471 0, 1472 GL_RGBA, GL_UNSIGNED_BYTE, 1473 blank) 1474 1475 texture = cls(texture_width, texture_height, target, id.value) 1476 texture.min_filter = min_filter 1477 texture.mag_filter = mag_filter 1478 if rectangle: 1479 texture._is_rectangle = True 1480 texture.tex_coords = (0., 0., 0., 1481 width, 0., 0., 1482 width, height, 0., 1483 0., height, 0.) 1484 1485 glFlush() 1486 1487 if texture_width == width and texture_height == height: 1488 return texture 1489 1490 return texture.get_region(0, 0, width, height) 1491 1492 @classmethod 1493 def create_for_size(cls, target, min_width, min_height, 1494 internalformat=None, min_filter=None, mag_filter=None): 1495 """Create a Texture with dimensions at least min_width, min_height. 1496 On return, the texture will be bound. 1497 1498 :Parameters: 1499 `target` : int 1500 GL constant giving texture target to use, typically 1501 ``GL_TEXTURE_2D``. 1502 `min_width` : int 1503 Minimum width of texture (may be increased to create a power 1504 of 2). 1505 `min_height` : int 1506 Minimum height of texture (may be increased to create a power 1507 of 2). 1508 `internalformat` : int 1509 GL constant giving internal format of texture; for example, 1510 ``GL_RGBA``. If unspecified, the texture will not be 1511 initialised (only the texture name will be created on the 1512 instance). If specified, the image will be initialised 1513 to this format with zero'd data. 1514 `min_filter` : int 1515 The minifaction filter used for this texture, commonly ``GL_LINEAR`` or ``GL_NEAREST`` 1516 `mag_filter` : int 1517 The magnification filter used for this texture, commonly ``GL_LINEAR`` or ``GL_NEAREST`` 1518 1519 :rtype: :py:class:`~pyglet.image.Texture` 1520 """ 1521 if target not in (GL_TEXTURE_RECTANGLE_NV, GL_TEXTURE_RECTANGLE_ARB): 1522 width = _nearest_pow2(min_width) 1523 height = _nearest_pow2(min_height) 1524 tex_coords = cls.tex_coords 1525 else: 1526 width = min_width 1527 height = min_height 1528 tex_coords = (0., 0., 0., 1529 width, 0., 0., 1530 width, height, 0., 1531 0., height, 0.) 1532 min_filter = min_filter or cls.default_min_filter 1533 mag_filter = mag_filter or cls.default_mag_filter 1534 id = GLuint() 1535 glGenTextures(1, byref(id)) 1536 glBindTexture(target, id.value) 1537 glTexParameteri(target, GL_TEXTURE_MIN_FILTER, min_filter) 1538 glTexParameteri(target, GL_TEXTURE_MAG_FILTER, mag_filter) 1539 1540 if internalformat is not None: 1541 blank = (GLubyte * (width * height * 4))() 1542 glTexImage2D(target, 0, 1543 internalformat, 1544 width, height, 1545 0, 1546 GL_RGBA, GL_UNSIGNED_BYTE, 1547 blank) 1548 glFlush() 1549 1550 texture = cls(width, height, target, id.value) 1551 texture.min_filter = min_filter 1552 texture.mag_filter = mag_filter 1553 texture.tex_coords = tex_coords 1554 return texture 1555 1556 def get_image_data(self, z=0): 1557 """Get the image data of this texture. 1558 1559 Changes to the returned instance will not be reflected in this 1560 texture. 1561 1562 :Parameters: 1563 `z` : int 1564 For 3D textures, the image slice to retrieve. 1565 1566 :rtype: :py:class:`~pyglet.image.ImageData` 1567 """ 1568 glBindTexture(self.target, self.id) 1569 1570 # Always extract complete RGBA data. Could check internalformat 1571 # to only extract used channels. XXX 1572 format = 'RGBA' 1573 gl_format = GL_RGBA 1574 1575 glPushClientAttrib(GL_CLIENT_PIXEL_STORE_BIT) 1576 glPixelStorei(GL_PACK_ALIGNMENT, 1) 1577 buffer = (GLubyte * (self.width * self.height * self.images * len(format)))() 1578 glGetTexImage(self.target, self.level, 1579 gl_format, GL_UNSIGNED_BYTE, buffer) 1580 glPopClientAttrib() 1581 1582 data = ImageData(self.width, self.height, format, buffer) 1583 if self.images > 1: 1584 data = data.get_region(0, z * self.height, self.width, self.height) 1585 return data 1586 1587 def get_texture(self, rectangle=False, force_rectangle=False): 1588 if force_rectangle and not self._is_rectangle: 1589 raise ImageException('Texture is not a rectangle.') 1590 return self 1591 1592 # no implementation of blit_to_texture yet (could use aux buffer) 1593 1594 def blit(self, x, y, z=0, width=None, height=None): 1595 t = self.tex_coords 1596 x1 = x - self.anchor_x 1597 y1 = y - self.anchor_y 1598 x2 = x1 + (width is None and self.width or width) 1599 y2 = y1 + (height is None and self.height or height) 1600 array = (GLfloat * 32)( 1601 t[0], t[1], t[2], 1., 1602 x1, y1, z, 1., 1603 t[3], t[4], t[5], 1., 1604 x2, y1, z, 1., 1605 t[6], t[7], t[8], 1., 1606 x2, y2, z, 1., 1607 t[9], t[10], t[11], 1., 1608 x1, y2, z, 1.) 1609 1610 glPushAttrib(GL_ENABLE_BIT) 1611 glEnable(self.target) 1612 glBindTexture(self.target, self.id) 1613 glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) 1614 glInterleavedArrays(GL_T4F_V4F, 0, array) 1615 glDrawArrays(GL_QUADS, 0, 4) 1616 glPopClientAttrib() 1617 glPopAttrib() 1618 1619 def blit_into(self, source, x, y, z): 1620 glBindTexture(self.target, self.id) 1621 source.blit_to_texture(self.target, self.level, x, y, z) 1622 1623 def get_region(self, x, y, width, height): 1624 return self.region_class(x, y, 0, width, height, self) 1625 1626 def get_transform(self, flip_x=False, flip_y=False, rotate=0): 1627 """Create a copy of this image applying a simple transformation. 1628 1629 The transformation is applied to the texture coordinates only; 1630 :py:meth:`~pyglet.image.ImageData.get_image_data` will return the untransformed data. The 1631 transformation is applied around the anchor point. 1632 1633 :Parameters: 1634 `flip_x` : bool 1635 If True, the returned image will be flipped horizontally. 1636 `flip_y` : bool 1637 If True, the returned image will be flipped vertically. 1638 `rotate` : int 1639 Degrees of clockwise rotation of the returned image. Only 1640 90-degree increments are supported. 1641 1642 :rtype: :py:class:`~pyglet.image.TextureRegion` 1643 """ 1644 transform = self.get_region(0, 0, self.width, self.height) 1645 bl, br, tr, tl = 0, 1, 2, 3 1646 transform.anchor_x = self.anchor_x 1647 transform.anchor_y = self.anchor_y 1648 if flip_x: 1649 bl, br, tl, tr = br, bl, tr, tl 1650 transform.anchor_x = self.width - self.anchor_x 1651 if flip_y: 1652 bl, br, tl, tr = tl, tr, bl, br 1653 transform.anchor_y = self.height - self.anchor_y 1654 rotate %= 360 1655 if rotate < 0: 1656 rotate += 360 1657 if rotate == 0: 1658 pass 1659 elif rotate == 90: 1660 bl, br, tr, tl = br, tr, tl, bl 1661 transform.anchor_x, transform.anchor_y = \ 1662 transform.anchor_y, \ 1663 transform.width - transform.anchor_x 1664 elif rotate == 180: 1665 bl, br, tr, tl = tr, tl, bl, br 1666 transform.anchor_x = transform.width - transform.anchor_x 1667 transform.anchor_y = transform.height - transform.anchor_y 1668 elif rotate == 270: 1669 bl, br, tr, tl = tl, bl, br, tr 1670 transform.anchor_x, transform.anchor_y = \ 1671 transform.height - transform.anchor_y, \ 1672 transform.anchor_x 1673 else: 1674 assert False, 'Only 90 degree rotations are supported.' 1675 if rotate in (90, 270): 1676 transform.width, transform.height = transform.height, transform.width 1677 transform._set_tex_coords_order(bl, br, tr, tl) 1678 return transform 1679 1680 def _set_tex_coords_order(self, bl, br, tr, tl): 1681 tex_coords = (self.tex_coords[:3], 1682 self.tex_coords[3:6], 1683 self.tex_coords[6:9], 1684 self.tex_coords[9:]) 1685 self.tex_coords = tex_coords[bl] + tex_coords[br] + tex_coords[tr] + tex_coords[tl] 1686 1687 order = self.tex_coords_order 1688 self.tex_coords_order = (order[bl], order[br], order[tr], order[tl]) 1689 1690 1691class TextureRegion(Texture): 1692 """A rectangular region of a texture, presented as if it were 1693 a separate texture. 1694 """ 1695 1696 def __init__(self, x, y, z, width, height, owner): 1697 super(TextureRegion, self).__init__(width, height, owner.target, owner.id) 1698 1699 self.x = x 1700 self.y = y 1701 self.z = z 1702 self.owner = owner 1703 owner_u1 = owner.tex_coords[0] 1704 owner_v1 = owner.tex_coords[1] 1705 owner_u2 = owner.tex_coords[3] 1706 owner_v2 = owner.tex_coords[7] 1707 scale_u = owner_u2 - owner_u1 1708 scale_v = owner_v2 - owner_v1 1709 u1 = x / owner.width * scale_u + owner_u1 1710 v1 = y / owner.height * scale_v + owner_v1 1711 u2 = (x + width) / owner.width * scale_u + owner_u1 1712 v2 = (y + height) / owner.height * scale_v + owner_v1 1713 r = z / owner.images + owner.tex_coords[2] 1714 self.tex_coords = (u1, v1, r, u2, v1, r, u2, v2, r, u1, v2, r) 1715 1716 def get_image_data(self): 1717 image_data = self.owner.get_image_data(self.z) 1718 return image_data.get_region(self.x, self.y, self.width, self.height) 1719 1720 def get_region(self, x, y, width, height): 1721 x += self.x 1722 y += self.y 1723 region = self.region_class(x, y, self.z, width, height, self.owner) 1724 region._set_tex_coords_order(*self.tex_coords_order) 1725 return region 1726 1727 def blit_into(self, source, x, y, z): 1728 self.owner.blit_into(source, x + self.x, y + self.y, z + self.z) 1729 1730 def __del__(self): 1731 # only the owner Texture should handle deletion 1732 pass 1733 1734 1735Texture.region_class = TextureRegion 1736 1737 1738class Texture3D(Texture, UniformTextureSequence): 1739 """A texture with more than one image slice. 1740 1741 Use `create_for_images` or `create_for_image_grid` classmethod to 1742 construct. 1743 """ 1744 item_width = 0 1745 item_height = 0 1746 items = () 1747 1748 @classmethod 1749 def create_for_images(cls, images, internalformat=GL_RGBA): 1750 item_width = images[0].width 1751 item_height = images[0].height 1752 for image in images: 1753 if image.width != item_width or image.height != item_height: 1754 raise ImageException('Images do not have same dimensions.') 1755 1756 depth = len(images) 1757 if not gl_info.have_version(2, 0): 1758 depth = _nearest_pow2(depth) 1759 1760 texture = cls.create_for_size(GL_TEXTURE_3D, item_width, item_height) 1761 if images[0].anchor_x or images[0].anchor_y: 1762 texture.anchor_x = images[0].anchor_x 1763 texture.anchor_y = images[0].anchor_y 1764 1765 texture.images = depth 1766 1767 blank = (GLubyte * (texture.width * texture.height * texture.images))() 1768 glBindTexture(texture.target, texture.id) 1769 glTexImage3D(texture.target, texture.level, 1770 internalformat, 1771 texture.width, texture.height, texture.images, 0, 1772 GL_ALPHA, GL_UNSIGNED_BYTE, 1773 blank) 1774 1775 items = [] 1776 for i, image in enumerate(images): 1777 item = cls.region_class(0, 0, i, item_width, item_height, texture) 1778 items.append(item) 1779 image.blit_to_texture(texture.target, texture.level, 1780 image.anchor_x, image.anchor_y, i) 1781 1782 glFlush() 1783 1784 texture.items = items 1785 texture.item_width = item_width 1786 texture.item_height = item_height 1787 return texture 1788 1789 @classmethod 1790 def create_for_image_grid(cls, grid, internalformat=GL_RGBA): 1791 return cls.create_for_images(grid[:], internalformat) 1792 1793 def __len__(self): 1794 return len(self.items) 1795 1796 def __getitem__(self, index): 1797 return self.items[index] 1798 1799 def __setitem__(self, index, value): 1800 if type(index) is slice: 1801 for item, image in zip(self[index], value): 1802 image.blit_to_texture(self.target, self.level, 1803 image.anchor_x, image.anchor_y, item.z) 1804 else: 1805 value.blit_to_texture(self.target, self.level, 1806 value.anchor_x, value.anchor_y, self[index].z) 1807 1808 def __iter__(self): 1809 return iter(self.items) 1810 1811 1812class TileableTexture(Texture): 1813 """A texture that can be tiled efficiently. 1814 1815 Use :py:class:`~pyglet.image.create_for_Image` classmethod to construct. 1816 """ 1817 1818 def __init__(self, width, height, target, id): 1819 if not _is_pow2(width) or not _is_pow2(height): 1820 raise ImageException( 1821 'TileableTexture requires dimensions that are powers of 2') 1822 super(TileableTexture, self).__init__(width, height, target, id) 1823 1824 def get_region(self, x, y, width, height): 1825 raise ImageException('Cannot get region of %r' % self) 1826 1827 def blit_tiled(self, x, y, z, width, height): 1828 """Blit this texture tiled over the given area. 1829 1830 The image will be tiled with the bottom-left corner of the destination 1831 rectangle aligned with the anchor point of this texture. 1832 """ 1833 u1 = self.anchor_x / self.width 1834 v1 = self.anchor_y / self.height 1835 u2 = u1 + width / self.width 1836 v2 = v1 + height / self.height 1837 w, h = width, height 1838 t = self.tex_coords 1839 array = (GLfloat * 32)( 1840 u1, v1, t[2], 1., 1841 x, y, z, 1., 1842 u2, v1, t[5], 1., 1843 x + w, y, z, 1., 1844 u2, v2, t[8], 1., 1845 x + w, y + h, z, 1., 1846 u1, v2, t[11], 1., 1847 x, y + h, z, 1.) 1848 1849 glPushAttrib(GL_ENABLE_BIT) 1850 glEnable(self.target) 1851 glBindTexture(self.target, self.id) 1852 glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT) 1853 glInterleavedArrays(GL_T4F_V4F, 0, array) 1854 glDrawArrays(GL_QUADS, 0, 4) 1855 glPopClientAttrib() 1856 glPopAttrib() 1857 1858 @classmethod 1859 def create_for_image(cls, image): 1860 if not _is_pow2(image.width) or not _is_pow2(image.height): 1861 # Potentially unnecessary conversion if a GL format exists. 1862 image = image.get_image_data() 1863 texture_width = _nearest_pow2(image.width) 1864 texture_height = _nearest_pow2(image.height) 1865 newdata = c_buffer(texture_width * texture_height * 4) 1866 gluScaleImage(GL_RGBA, 1867 image.width, image.height, 1868 GL_UNSIGNED_BYTE, 1869 image.get_data('RGBA', image.width * 4), 1870 texture_width, 1871 texture_height, 1872 GL_UNSIGNED_BYTE, 1873 newdata) 1874 image = ImageData(texture_width, texture_height, 'RGBA', 1875 newdata) 1876 1877 image = image.get_image_data() 1878 return image.create_texture(cls) 1879 1880 1881class DepthTexture(Texture): 1882 """A texture with depth samples (typically 24-bit).""" 1883 1884 def blit_into(self, source, x, y, z): 1885 glBindTexture(self.target, self.id) 1886 source.blit_to_texture(self.level, x, y, z) 1887 1888 1889class BufferManager: 1890 """Manages the set of framebuffers for a context. 1891 1892 Use :py:func:`~pyglet.image.get_buffer_manager` to obtain the instance of this class for the 1893 current context. 1894 """ 1895 1896 def __init__(self): 1897 self.color_buffer = None 1898 self.depth_buffer = None 1899 1900 aux_buffers = GLint() 1901 glGetIntegerv(GL_AUX_BUFFERS, byref(aux_buffers)) 1902 self.free_aux_buffers = [GL_AUX0, 1903 GL_AUX1, 1904 GL_AUX2, 1905 GL_AUX3][:aux_buffers.value] 1906 1907 stencil_bits = GLint() 1908 glGetIntegerv(GL_STENCIL_BITS, byref(stencil_bits)) 1909 self.free_stencil_bits = list(range(stencil_bits.value)) 1910 1911 self.refs = [] 1912 1913 def get_viewport(self): 1914 """Get the current OpenGL viewport dimensions. 1915 1916 :rtype: 4-tuple of float. 1917 :return: Left, top, right and bottom dimensions. 1918 """ 1919 viewport = (GLint * 4)() 1920 glGetIntegerv(GL_VIEWPORT, viewport) 1921 return viewport 1922 1923 def get_color_buffer(self): 1924 """Get the color buffer. 1925 1926 :rtype: :py:class:`~pyglet.image.ColorBufferImage` 1927 """ 1928 viewport = self.get_viewport() 1929 viewport_width = viewport[2] 1930 viewport_height = viewport[3] 1931 if (not self.color_buffer or 1932 viewport_width != self.color_buffer.width or 1933 viewport_height != self.color_buffer.height): 1934 self.color_buffer = ColorBufferImage(*viewport) 1935 return self.color_buffer 1936 1937 def get_aux_buffer(self): 1938 """Get a free auxiliary buffer. 1939 1940 If not aux buffers are available, `ImageException` is raised. Buffers 1941 are released when they are garbage collected. 1942 1943 :rtype: :py:class:`~pyglet.image.ColorBufferImage` 1944 """ 1945 if not self.free_aux_buffers: 1946 raise ImageException('No free aux buffer is available.') 1947 1948 gl_buffer = self.free_aux_buffers.pop(0) 1949 viewport = self.get_viewport() 1950 buffer = ColorBufferImage(*viewport) 1951 buffer.gl_buffer = gl_buffer 1952 1953 def release_buffer(ref, self=self): 1954 self.free_aux_buffers.insert(0, gl_buffer) 1955 1956 self.refs.append(weakref.ref(buffer, release_buffer)) 1957 1958 return buffer 1959 1960 def get_depth_buffer(self): 1961 """Get the depth buffer. 1962 1963 :rtype: :py:class:`~pyglet.image.DepthBufferImage` 1964 """ 1965 viewport = self.get_viewport() 1966 viewport_width = viewport[2] 1967 viewport_height = viewport[3] 1968 if (not self.depth_buffer or 1969 viewport_width != self.depth_buffer.width or 1970 viewport_height != self.depth_buffer.height): 1971 self.depth_buffer = DepthBufferImage(*viewport) 1972 return self.depth_buffer 1973 1974 def get_buffer_mask(self): 1975 """Get a free bitmask buffer. 1976 1977 A bitmask buffer is a buffer referencing a single bit in the stencil 1978 buffer. If no bits are free, `ImageException` is raised. Bits are 1979 released when the bitmask buffer is garbage collected. 1980 1981 :rtype: :py:class:`~pyglet.image.BufferImageMask` 1982 """ 1983 if not self.free_stencil_bits: 1984 raise ImageException('No free stencil bits are available.') 1985 1986 stencil_bit = self.free_stencil_bits.pop(0) 1987 x, y, width, height = self.get_viewport() 1988 buffer = BufferImageMask(x, y, width, height) 1989 buffer.stencil_bit = stencil_bit 1990 1991 def release_buffer(ref, self=self): 1992 self.free_stencil_bits.insert(0, stencil_bit) 1993 1994 self.refs.append(weakref.ref(buffer, release_buffer)) 1995 1996 return buffer 1997 1998 1999def get_buffer_manager(): 2000 """Get the buffer manager for the current OpenGL context. 2001 2002 :rtype: :py:class:`~pyglet.image.BufferManager` 2003 """ 2004 context = gl.current_context 2005 if not hasattr(context, 'image_buffer_manager'): 2006 context.image_buffer_manager = BufferManager() 2007 return context.image_buffer_manager 2008 2009 2010# XXX BufferImage could be generalised to support EXT_framebuffer_object's 2011# renderbuffer. 2012class BufferImage(AbstractImage): 2013 """An abstract framebuffer. 2014 """ 2015 #: The OpenGL read and write target for this buffer. 2016 gl_buffer = GL_BACK 2017 2018 #: The OpenGL format constant for image data. 2019 gl_format = 0 2020 2021 #: The format string used for image data. 2022 format = '' 2023 2024 owner = None 2025 2026 # TODO: enable methods 2027 2028 def __init__(self, x, y, width, height): 2029 self.x = x 2030 self.y = y 2031 self.width = width 2032 self.height = height 2033 2034 def get_image_data(self): 2035 buffer = (GLubyte * (len(self.format) * self.width * self.height))() 2036 2037 x = self.x 2038 y = self.y 2039 if self.owner: 2040 x += self.owner.x 2041 y += self.owner.y 2042 2043 glReadBuffer(self.gl_buffer) 2044 glPushClientAttrib(GL_CLIENT_PIXEL_STORE_BIT) 2045 glPixelStorei(GL_PACK_ALIGNMENT, 1) 2046 glReadPixels(x, y, self.width, self.height, 2047 self.gl_format, GL_UNSIGNED_BYTE, buffer) 2048 glPopClientAttrib() 2049 2050 return ImageData(self.width, self.height, self.format, buffer) 2051 2052 def get_region(self, x, y, width, height): 2053 if self.owner: 2054 return self.owner.get_region(x + self.x, y + self.y, width, height) 2055 2056 region = self.__class__(x + self.x, y + self.y, width, height) 2057 region.gl_buffer = self.gl_buffer 2058 region.owner = self 2059 return region 2060 2061 2062class ColorBufferImage(BufferImage): 2063 """A color framebuffer. 2064 2065 This class is used to wrap both the primary color buffer (i.e., the back 2066 buffer) or any one of the auxiliary buffers. 2067 """ 2068 gl_format = GL_RGBA 2069 format = 'RGBA' 2070 2071 def get_texture(self, rectangle=False, force_rectangle=False): 2072 texture = Texture.create(self.width, self.height, GL_RGBA, 2073 rectangle, force_rectangle) 2074 self.blit_to_texture(texture.target, texture.level, 2075 self.anchor_x, self.anchor_y, 0) 2076 return texture 2077 2078 def blit_to_texture(self, target, level, x, y, z): 2079 glReadBuffer(self.gl_buffer) 2080 glCopyTexSubImage2D(target, level, 2081 x - self.anchor_x, y - self.anchor_y, 2082 self.x, self.y, self.width, self.height) 2083 2084 2085class DepthBufferImage(BufferImage): 2086 """The depth buffer. 2087 """ 2088 gl_format = GL_DEPTH_COMPONENT 2089 format = 'L' 2090 2091 def get_texture(self, rectangle=False, force_rectangle=False): 2092 assert rectangle == False and force_rectangle == False, \ 2093 'Depth textures cannot be rectangular' 2094 if not _is_pow2(self.width) or not _is_pow2(self.height): 2095 raise ImageException( 2096 'Depth texture requires that buffer dimensions be powers of 2') 2097 2098 texture = DepthTexture.create_for_size(GL_TEXTURE_2D, self.width, self.height) 2099 if self.anchor_x or self.anchor_y: 2100 texture.anchor_x = self.anchor_x 2101 texture.anchor_y = self.anchor_y 2102 2103 glReadBuffer(self.gl_buffer) 2104 glCopyTexImage2D(texture.target, 0, 2105 GL_DEPTH_COMPONENT, 2106 self.x, self.y, self.width, self.height, 2107 0) 2108 return texture 2109 2110 def blit_to_texture(self, target, level, x, y, z): 2111 glReadBuffer(self.gl_buffer) 2112 glCopyTexSubImage2D(target, level, 2113 x - self.anchor_x, y - self.anchor_y, 2114 self.x, self.y, self.width, self.height) 2115 2116 2117class BufferImageMask(BufferImage): 2118 """A single bit of the stencil buffer. 2119 """ 2120 gl_format = GL_STENCIL_INDEX 2121 format = 'L' 2122 2123 # TODO mask methods 2124 2125 2126class ImageGrid(AbstractImage, AbstractImageSequence): 2127 """An imaginary grid placed over an image allowing easy access to 2128 regular regions of that image. 2129 2130 The grid can be accessed either as a complete image, or as a sequence 2131 of images. The most useful applications are to access the grid 2132 as a :py:class:`~pyglet.image.TextureGrid`:: 2133 2134 image_grid = ImageGrid(...) 2135 texture_grid = image_grid.get_texture_sequence() 2136 2137 or as a :py:class:`~pyglet.image.Texture3D`:: 2138 2139 image_grid = ImageGrid(...) 2140 texture_3d = Texture3D.create_for_image_grid(image_grid) 2141 2142 """ 2143 _items = () 2144 _texture_grid = None 2145 2146 def __init__(self, image, rows, columns, 2147 item_width=None, item_height=None, 2148 row_padding=0, column_padding=0): 2149 """Construct a grid for the given image. 2150 2151 You can specify parameters for the grid, for example setting 2152 the padding between cells. Grids are always aligned to the 2153 bottom-left corner of the image. 2154 2155 :Parameters: 2156 `image` : AbstractImage 2157 Image over which to construct the grid. 2158 `rows` : int 2159 Number of rows in the grid. 2160 `columns` : int 2161 Number of columns in the grid. 2162 `item_width` : int 2163 Width of each column. If unspecified, is calculated such 2164 that the entire image width is used. 2165 `item_height` : int 2166 Height of each row. If unspecified, is calculated such that 2167 the entire image height is used. 2168 `row_padding` : int 2169 Pixels separating adjacent rows. The padding is only 2170 inserted between rows, not at the edges of the grid. 2171 `column_padding` : int 2172 Pixels separating adjacent columns. The padding is only 2173 inserted between columns, not at the edges of the grid. 2174 """ 2175 super(ImageGrid, self).__init__(image.width, image.height) 2176 2177 if item_width is None: 2178 item_width = (image.width - column_padding * (columns - 1)) // columns 2179 if item_height is None: 2180 item_height = (image.height - row_padding * (rows - 1)) // rows 2181 self.image = image 2182 self.rows = rows 2183 self.columns = columns 2184 self.item_width = item_width 2185 self.item_height = item_height 2186 self.row_padding = row_padding 2187 self.column_padding = column_padding 2188 2189 def get_texture(self, rectangle=False, force_rectangle=False): 2190 return self.image.get_texture(rectangle, force_rectangle) 2191 2192 def get_image_data(self): 2193 return self.image.get_image_data() 2194 2195 def get_texture_sequence(self): 2196 if not self._texture_grid: 2197 self._texture_grid = TextureGrid(self) 2198 return self._texture_grid 2199 2200 def __len__(self): 2201 return self.rows * self.columns 2202 2203 def _update_items(self): 2204 if not self._items: 2205 self._items = [] 2206 y = 0 2207 for row in range(self.rows): 2208 x = 0 2209 for col in range(self.columns): 2210 self._items.append(self.image.get_region( 2211 x, y, self.item_width, self.item_height)) 2212 x += self.item_width + self.column_padding 2213 y += self.item_height + self.row_padding 2214 2215 def __getitem__(self, index): 2216 self._update_items() 2217 if type(index) is tuple: 2218 row, column = index 2219 assert row >= 0 and column >= 0 and row < self.rows and column < self.columns 2220 return self._items[row * self.columns + column] 2221 else: 2222 return self._items[index] 2223 2224 def __iter__(self): 2225 self._update_items() 2226 return iter(self._items) 2227 2228 2229class TextureGrid(TextureRegion, UniformTextureSequence): 2230 """A texture containing a regular grid of texture regions. 2231 2232 To construct, create an :py:class:`~pyglet.image.ImageGrid` first:: 2233 2234 image_grid = ImageGrid(...) 2235 texture_grid = TextureGrid(image_grid) 2236 2237 The texture grid can be accessed as a single texture, or as a sequence 2238 of :py:class:`~pyglet.image.TextureRegion`. When accessing as a sequence, you can specify 2239 integer indexes, in which the images are arranged in rows from the 2240 bottom-left to the top-right:: 2241 2242 # assume the texture_grid is 3x3: 2243 current_texture = texture_grid[3] # get the middle-left image 2244 2245 You can also specify tuples in the sequence methods, which are addressed 2246 as ``row, column``:: 2247 2248 # equivalent to the previous example: 2249 current_texture = texture_grid[1, 0] 2250 2251 When using tuples in a slice, the returned sequence is over the 2252 rectangular region defined by the slice:: 2253 2254 # returns center, center-right, center-top, top-right images in that 2255 # order: 2256 images = texture_grid[(1,1):] 2257 # equivalent to 2258 images = texture_grid[(1,1):(3,3)] 2259 2260 """ 2261 items = () 2262 rows = 1 2263 columns = 1 2264 item_width = 0 2265 item_height = 0 2266 2267 def __init__(self, grid): 2268 image = grid.get_texture() 2269 if isinstance(image, TextureRegion): 2270 owner = image.owner 2271 else: 2272 owner = image 2273 2274 super(TextureGrid, self).__init__( 2275 image.x, image.y, image.z, image.width, image.height, owner) 2276 2277 items = [] 2278 y = 0 2279 for row in range(grid.rows): 2280 x = 0 2281 for col in range(grid.columns): 2282 items.append( 2283 self.get_region(x, y, grid.item_width, grid.item_height)) 2284 x += grid.item_width + grid.column_padding 2285 y += grid.item_height + grid.row_padding 2286 2287 self.items = items 2288 self.rows = grid.rows 2289 self.columns = grid.columns 2290 self.item_width = grid.item_width 2291 self.item_height = grid.item_height 2292 2293 def get(self, row, column): 2294 return self[(row, column)] 2295 2296 def __getitem__(self, index): 2297 if type(index) is slice: 2298 if type(index.start) is not tuple and type(index.stop) is not tuple: 2299 return self.items[index] 2300 else: 2301 row1 = 0 2302 col1 = 0 2303 row2 = self.rows 2304 col2 = self.columns 2305 if type(index.start) is tuple: 2306 row1, col1 = index.start 2307 elif type(index.start) is int: 2308 row1 = index.start // self.columns 2309 col1 = index.start % self.columns 2310 assert row1 >= 0 and col1 >= 0 and row1 < self.rows and col1 < self.columns 2311 2312 if type(index.stop) is tuple: 2313 row2, col2 = index.stop 2314 elif type(index.stop) is int: 2315 row2 = index.stop // self.columns 2316 col2 = index.stop % self.columns 2317 assert row2 >= 0 and col2 >= 0 and row2 <= self.rows and col2 <= self.columns 2318 2319 result = [] 2320 i = row1 * self.columns 2321 for row in range(row1, row2): 2322 result += self.items[i + col1:i + col2] 2323 i += self.columns 2324 return result 2325 else: 2326 if type(index) is tuple: 2327 row, column = index 2328 assert row >= 0 and column >= 0 and row < self.rows and column < self.columns 2329 return self.items[row * self.columns + column] 2330 elif type(index) is int: 2331 return self.items[index] 2332 2333 def __setitem__(self, index, value): 2334 if type(index) is slice: 2335 for region, image in zip(self[index], value): 2336 if image.width != self.item_width or image.height != self.item_height: 2337 raise ImageException('Image has incorrect dimensions') 2338 image.blit_into(region, image.anchor_x, image.anchor_y, 0) 2339 else: 2340 image = value 2341 if image.width != self.item_width or image.height != self.item_height: 2342 raise ImageException('Image has incorrect dimensions') 2343 image.blit_into(self[index], image.anchor_x, image.anchor_y, 0) 2344 2345 def __len__(self): 2346 return len(self.items) 2347 2348 def __iter__(self): 2349 return iter(self.items) 2350 2351 2352# Initialise default codecs 2353add_default_image_codecs() 2354