diff --git a/arcade/texture_atlas/base.py b/arcade/texture_atlas/base.py index f5f7883b8..683219b45 100644 --- a/arcade/texture_atlas/base.py +++ b/arcade/texture_atlas/base.py @@ -969,6 +969,47 @@ def calculate_minimum_size(cls, textures: Sequence["Texture"], border: int = 1): return size, size + def get_texture_image(self, texture: "Texture") -> Image.Image: + """ + Get a Pillow image of a texture's region in the atlas. + This can be used to inspect the contents of the atlas + or to save the texture to disk. + + :param Texture texture: The texture to get the image for + :return: A pillow image containing the pixel data in the atlas + """ + region = self.get_image_region_info(texture.image_data.hash) + viewport = ( + region.x + self._border, + region.y + self._border, + region.width, + region.height, + ) + data = self.fbo.read(viewport=viewport, components=4) + return Image.frombytes("RGBA", (region.width, region.height), data) + + def sync_texture_image(self, texture: "Texture") -> None: + """ + Updates a texture's image with the contents in the + texture atlas. This is usually not needed, but if + you have altered a texture in the atlas directly + this can be used to copy the image data back into + the texture. + + Updating the image will not change the texture's + hash or the texture's hit box points. + + .. warning:: + + This method is somewhat expensive and should be used sparingly. + Altering the internal image of a texture is not recommended + unless you know exactly what you're doing. Textures are + supposed to be immutable. + + :param Texture texture: The texture to update + """ + texture.image_data.image = self.get_texture_image(texture) + def to_image( self, flip: bool = False, diff --git a/doc/programming_guide/release_notes.rst b/doc/programming_guide/release_notes.rst index 7b4fa8749..d97511c38 100644 --- a/doc/programming_guide/release_notes.rst +++ b/doc/programming_guide/release_notes.rst @@ -204,6 +204,9 @@ Changes ``layer_options`` dictionary. If no custom atlas is provided, then the global default atlas will be used (This is how it works pre-Arcade 3.0). * Fix for animated tiles from sprite sheets + * TextureAtlas: Added ``sync_texture_image`` method to sync the texture in the atlas back into + the internal pillow image in the ``arcade.Texture``. + * TextureAtlas: Added ``get_texture_image`` method to get pixel data of a texture in the atlas as a pillow image. * Collision Detection diff --git a/tests/conftest.py b/tests/conftest.py index c65d1668d..7d7bc406b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import os from pathlib import Path +import gc if os.environ.get("ARCADE_PYTEST_USE_RUST"): import arcade_accelerate # pyright: ignore [reportMissingImports] @@ -42,6 +43,7 @@ def prepare_window(window: arcade.Window): window.clear() ctx.gc_mode = "context_gc" ctx.gc() + gc.collect() # Ensure no old functions are lingering window.on_draw = lambda: None diff --git a/tests/unit/atlas/test_sync_texture_image.py b/tests/unit/atlas/test_sync_texture_image.py new file mode 100644 index 000000000..49646c6d4 --- /dev/null +++ b/tests/unit/atlas/test_sync_texture_image.py @@ -0,0 +1,25 @@ +""" +Test syncing atlas textures back into PIL images. +""" +from PIL import Image +import arcade +from arcade.resources import resolve + + +def test_sync(ctx): + im_1 = Image.open(resolve(":assets:images/cards/cardClubs2.png")) + im_2 = Image.open(resolve(":assets:images/cards/cardDiamonds8.png")) + tex = arcade.Texture(im_1) + + atlas = arcade.TextureAtlas((256, 256)) + atlas.add(tex) + + # Write the second image over the first one + region = atlas.get_image_region_info(tex.image_data.hash) + atlas.write_image(im_2, region.x, region.y) + + # Sync the texture back into the PIL image. + # It should contain the same image as tex_2 + atlas.sync_texture_image(tex) + + assert tex.image.tobytes() == im_2.tobytes()