# ---------------------------------------------------------------------------- # pyglet # Copyright (c) 2006-2008 Alex Holkner # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the # distribution. # * Neither the name of pyglet nor the names of its # contributors may be used to endorse or promote products # derived from this software without specific prior written # permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ---------------------------------------------------------------------------- '''Group multiple small images into larger textures. This module is used by `pyglet.resource` to efficiently pack small images into larger textures. `TextureAtlas` maintains one texture; `TextureBin` manages a collection of atlases of a given size. Example usage:: # Load images from disk car_image = pyglet.image.load('car.png') boat_image = pyglet.image.load('boat.png') # Pack these images into one or more textures bin = TextureBin() car_texture = bin.add(car_image) boat_texture = bin.add(boat_image) The result of `TextureBin.add` is a `TextureRegion` containing the image. Once added, an image cannot be removed from a bin (or an atlas); nor can a list of images be obtained from a given bin or atlas -- it is the application's responsibility to keep track of the regions returned by the ``add`` methods. :since: pyglet 1.1 ''' __docformat__ = 'restructuredtext' __version__ = '$Id: $' import pyglet class AllocatorException(Exception): '''The allocator does not have sufficient free space for the requested image size.''' pass class _Strip(object): def __init__(self, y, max_height): self.x = 0 self.y = y self.max_height = max_height self.y2 = y def add(self, width, height): assert width > 0 and height > 0 assert height <= self.max_height x, y = self.x, self.y self.x += width self.y2 = max(self.y + height, self.y2) return x, y def compact(self): self.max_height = self.y2 - self.y class Allocator(object): '''Rectangular area allocation algorithm. Initialise with a given ``width`` and ``height``, then repeatedly call `alloc` to retrieve free regions of the area and protect that area from future allocations. `Allocator` uses a fairly simple strips-based algorithm. It performs best when rectangles are allocated in decreasing height order. ''' def __init__(self, width, height): '''Create an `Allocator` of the given size. :Parameters: `width` : int Width of the allocation region. `height` : int Height of the allocation region. ''' assert width > 0 and height > 0 self.width = width self.height = height self.strips = [_Strip(0, height)] self.used_area = 0 def alloc(self, width, height): '''Get a free area in the allocator of the given size. After calling `alloc`, the requested area will no longer be used. If there is not enough room to fit the given area `AllocatorException` is raised. :Parameters: `width` : int Width of the area to allocate. `height` : int Height of the area to allocate. :rtype: int, int :return: The X and Y coordinates of the bottom-left corner of the allocated region. ''' for strip in self.strips: if self.width - strip.x >= width and strip.max_height >= height: self.used_area += width * height return strip.add(width, height) if self.width >= width and self.height - strip.y2 >= height: self.used_area += width * height strip.compact() newstrip = _Strip(strip.y2, self.height - strip.y2) self.strips.append(newstrip) return newstrip.add(width, height) raise AllocatorException('No more space in %r for box %dx%d' % ( self, width, height)) def get_usage(self): '''Get the fraction of area already allocated. This method is useful for debugging and profiling only. :rtype: float ''' return self.used_area / float(self.width * self.height) def get_fragmentation(self): '''Get the fraction of area that's unlikely to ever be used, based on current allocation behaviour. This method is useful for debugging and profiling only. :rtype: float ''' # The total unused area in each compacted strip is summed. if not self.strips: return 0. possible_area = self.strips[-1].y2 * width return 1.0 - self.used_area / float(possible_area) class TextureAtlas(object): '''Collection of images within a texture. ''' def __init__(self, width=256, height=256): '''Create a texture atlas of the given size. :Parameters: `width` : int Width of the underlying texture. `height` : int Height of the underlying texture. ''' self.texture = pyglet.image.Texture.create( width, height, pyglet.gl.GL_RGBA, rectangle=True) self.allocator = Allocator(width, height) def add(self, img): '''Add an image to the atlas. This method will fail if the given image cannot be transferred directly to a texture (for example, if it is another texture). `ImageData` is the usual image type for this method. `AllocatorException` will be raised if there is no room in the atlas for the image. :Parameters: `img` : `AbstractImage` The image to add. :rtype: `TextureRegion` :return: The region of the atlas containing the newly added image. ''' x, y = self.allocator.alloc(img.width, img.height) self.texture.blit_into(img, x, y, 0) region = self.texture.get_region(x, y, img.width, img.height) return region class TextureBin(object): '''Collection of texture atlases. `TextureBin` maintains a collection of texture atlases, and creates new ones as necessary to accomodate images added to the bin. ''' def __init__(self, texture_width=256, texture_height=256): '''Create a texture bin for holding atlases of the given size. :Parameters: `texture_width` : int Width of texture atlases to create. `texture_height` : int Height of texture atlases to create. ''' self.atlases = [] self.texture_width = texture_width self.texture_height = texture_height def add(self, img): '''Add an image into this texture bin. This method calls `TextureAtlas.add` for the first atlas that has room for the image. `AllocatorException` is raised if the image exceeds the dimensions of ``texture_width`` and ``texture_height``. :Parameters: `img` : `AbstractImage` The image to add. :rtype: `TextureRegion` :return: The region of an atlas containing the newly added image. ''' for atlas in list(self.atlases): try: return atlas.add(img) except AllocatorException: # Remove atlases that are no longer useful (this is so their # textures can later be freed if the images inside them get # collected). if img.width < 64 and img.height < 64: self.atlases.remove(atlas) atlas = TextureAtlas(self.texture_width, self.texture_height) self.atlases.append(atlas) return atlas.add(img)