from pathlib import Path
import PIL.Image
import PIL.ImageDraw
import PIL.ImageOps
import arcade
from arcade import hitbox
from arcade.cache import (
HitBoxCache,
ImageDataCache,
TextureCache,
)
from arcade.texture import ImageData, SpriteSheet
from arcade.types.rect import Rect
from .texture import Texture
[docs]
class TextureCacheManager:
"""
A simple manager wrapping texture, image data and hit box caches
with convenient methods for loading textures and sprite sheets.
Args:
hit_box_cache:
Optional hit box cache to use. If not specified, a new cache will be created.
image_data_cache:
Optional image data cache to use. If not specified, a new cache will be created.
texture_cache:
Optional texture cache to use. If not specified, a new cache will be created
"""
def __init__(
self,
hit_box_cache: HitBoxCache | None = None,
image_data_cache: ImageDataCache | None = None,
texture_cache: TextureCache | None = None,
):
self._sprite_sheets: dict[str, SpriteSheet] = {}
self._hit_box_cache = hit_box_cache or HitBoxCache()
self._image_data_cache = image_data_cache or ImageDataCache()
self._texture_cache = texture_cache or TextureCache()
@property
def hit_box_cache(self) -> HitBoxCache:
"""Get or set the current hit box cache."""
return self._hit_box_cache
@hit_box_cache.setter
def hit_box_cache(self, value: HitBoxCache):
self._hit_box_cache = value
@property
def image_data_cache(self) -> ImageDataCache:
"""Cache for image data."""
return self._image_data_cache
@property
def texture_cache(self) -> TextureCache:
"""Cache for textures."""
return self._texture_cache
[docs]
def flush(
self,
sprite_sheets: bool = True,
textures: bool = True,
image_data: bool = True,
hit_boxes: bool = False,
):
"""
Remove contents from the texture manager.
Args:
sprite_sheets:
If ``True``, sprite sheets will be flushed.
textures:
If ``True``, textures will be flushed.
image_data:
If ``True``, image data will be flushed.
hit_boxes:
If ``True``, hit boxes will be flushed.
"""
if sprite_sheets:
self._sprite_sheets.clear()
if textures:
self._texture_cache.flush()
if image_data:
self._image_data_cache.flush()
if hit_boxes:
self._hit_box_cache.flush()
def _get_real_path(self, path: str | Path) -> Path:
"""
Resolve the path to the file.
Args:
path: Path to the file
"""
if isinstance(path, str):
return arcade.resources.resolve(path)
elif isinstance(path, Path):
return path
else:
raise TypeError(f"Invalid path type: {type(path)} for {path}")
[docs]
def load_or_get_spritesheet(self, path: str | Path) -> SpriteSheet:
"""
Load a sprite sheet from disk, or return a cached version.
Note that any texture sliced from the sprite sheet will be cached.
if this is not the desirable behavior, use :meth:`load_or_get_spritesheet_texture`.
Args:
path: Path to the sprite sheet image
"""
real_path = self._get_real_path(path)
real_path_str = str(real_path)
sprite_sheet = self._sprite_sheets.get(real_path_str)
if sprite_sheet is None:
sprite_sheet = arcade.SpriteSheet(real_path)
self._sprite_sheets[real_path_str] = sprite_sheet
return sprite_sheet
[docs]
def load_or_get_spritesheet_texture(
self,
path: str | Path,
rect: Rect,
hit_box_algorithm: hitbox.HitBoxAlgorithm | None = None,
) -> Texture:
"""
Slice out a texture slice from a sprite sheet.
* If the spritesheet is not already loaded, it will be loaded and cached.
* If the sliced texture is already cached, it will be returned instead.
Args:
path:
Path to the sprite sheet image
rect:
Slice of the texture in the sprite sheet.
hit_box_algorithm:
Hit box algorithm to use. If not specified, the global default will be used.
"""
real_path = self._get_real_path(path)
texture = self._texture_cache.get_texture_by_filepath(real_path, crop=rect.lbwh_int)
if texture:
return texture
# check if sprite sheet is cached and load if not
sprite_sheet = self.load_or_get_spritesheet(real_path)
# slice out the texture and cache + return
texture = sprite_sheet.get_texture(rect, hit_box_algorithm=hit_box_algorithm)
self._texture_cache.put(texture)
if texture.image_cache_name:
self._image_data_cache.put(texture.image_cache_name, texture.image_data)
# Add to image data cache
self._image_data_cache.put(
Texture.create_image_cache_name(real_path, rect.lbwh_int),
texture.image_data,
)
return texture
[docs]
def load_or_get_image(
self,
path: str | Path,
hash: str | None = None,
mode="RGBA",
) -> ImageData:
"""
Loads a complete image from disk or return a cached version.
Args:
path:
Path of the file to load.
hash:
Optional override for image hash
mode:
The mode to use for the image. Default is "RGBA".
"""
real_path = self._get_real_path(path)
name = Texture.create_image_cache_name(real_path)
im_data = self._image_data_cache.get(name)
if im_data:
return im_data
image = PIL.Image.open(real_path).convert(mode)
im_data = ImageData(image, hash=hash)
self._image_data_cache.put(name, im_data)
return im_data
[docs]
def load_or_get_texture(
self,
file_path: str | Path,
*,
x: int = 0,
y: int = 0,
width: int = 0,
height: int = 0,
hit_box_algorithm: hitbox.HitBoxAlgorithm | None = None,
) -> Texture:
"""
Load an image from disk and create a texture. If the image is already
loaded, return the cached version.
The ``x``, ``y``, ``width``, and ``height`` parameters are used to
specify a sub-rectangle of the image to load. If not specified, the
entire image is loaded.
Args:
file_path:
Path to the image file.
x:
X coordinate of the texture in the image.
y:
Y coordinate of the texture in the image.
width:
Width of the texture in the image.
height:
Height of the texture in the image.
hit_box_algorithm:
The hit box algorithm to use for this texture. If not specified,
the global default will be used.
"""
real_path = self._get_real_path(file_path)
return self._load_or_get_texture(
real_path,
hit_box_algorithm=hit_box_algorithm,
crop=(x, y, width, height),
)
def _load_or_get_texture(
self,
file_path: Path,
hit_box_algorithm: hitbox.HitBoxAlgorithm | None = None,
crop: tuple[int, int, int, int] = (0, 0, 0, 0),
hash: str | None = None,
) -> Texture:
"""Load a texture, or return a cached version if it's already loaded."""
hit_box_algorithm = hit_box_algorithm or hitbox.algo_default
image_data: ImageData | None = None
texture = None
# Load the image data from disk or get from cache
image_data, cached = self._load_or_get_image(file_path, hash=hash)
# If the image was fetched from cache we might have cached texture
if cached:
texture = self._texture_cache.get_with_config(image_data.hash, hit_box_algorithm)
# If we still don't have a texture, create it
if texture is None:
texture = Texture(image_data, hit_box_algorithm=hit_box_algorithm)
texture.file_path = file_path
texture.crop_values = crop
self._texture_cache.put(texture)
# If we have crop values we need to dig deeper looking for cached versions
if crop != (0, 0, 0, 0):
image_data = self._image_data_cache.get(
Texture.create_image_cache_name(file_path, crop)
)
# If we don't have and cached image data we can crop from the base texture
if image_data is None:
texture = texture.crop(*crop)
self._texture_cache.put(texture)
self._image_data_cache.put(
Texture.create_image_cache_name(file_path, crop), texture.image_data
)
else:
# We might have a texture for this image data
texture = self._texture_cache.get_with_config(image_data.hash, hit_box_algorithm)
if texture is None:
texture = Texture(image_data, hit_box_algorithm=hit_box_algorithm)
texture.file_path = file_path
texture.crop_values = crop
self._texture_cache.put(texture)
return texture
def _load_or_get_image(
self,
file_path: Path,
hash: str | None = None,
mode: str = "RGBA",
) -> tuple[ImageData, bool]:
"""
Load an image, or return a cached version
Args:
file_path:
Path to image
hash:
Hash of the image
mode:
The image mode to use (RGBA, RGB, etc.)
Returns:
Tuple of image data and a boolean indicating if the
image was fetched from cache
"""
file_path_str = str(file_path)
cached = True
# Do we have cached image data for this file?
image_data = self._image_data_cache.get(Texture.create_image_cache_name(file_path_str))
if not image_data:
cached = False
im = PIL.Image.open(file_path).convert(mode)
image_data = ImageData(im, hash)
self._image_data_cache.put(
Texture.create_image_cache_name(file_path_str),
image_data,
)
return image_data, cached