Table Of Contents

Source code for kivy.core.image

'''
Image
=====

Core classes for loading images and converting them to a
:class:`~kivy.graphics.texture.Texture`. The raw image data can be keep in
memory for further access.

.. versionchanged:: 1.11.0

    Add support for argb and abgr image data

In-memory image loading
-----------------------

.. versionadded:: 1.9.0

    Official support for in-memory loading. Not all the providers support it,
    but currently SDL3, pil and imageio work.

To load an image with a filename, you would usually do::

    from kivy.core.image import Image as CoreImage
    im = CoreImage("image.png")

You can also load the image data directly from a memory block. Instead of
passing the filename, you'll need to pass the data as a BytesIO object
together with an "ext" parameter. Both are mandatory::

    import io
    from kivy.core.image import Image as CoreImage
    data = io.BytesIO(open("image.png", "rb").read())
    im = CoreImage(data, ext="png")

By default, the image will not be cached as our internal cache requires a
filename. If you want caching, add a filename that represents your file (it
will be used only for caching)::

    import io
    from kivy.core.image import Image as CoreImage
    data = io.BytesIO(open("image.png", "rb").read())
    im = CoreImage(data, ext="png", filename="image.png")

Data URI loading
----------------

Images can be loaded directly from data URIs without needing to save them to disk first.
This is useful for embedding small images directly in your code or loading images
from web APIs that return base64-encoded data.

Data URIs must follow the standard format::

    data:image/<type>[;<options>],<data>

Where:
    - `<type>`: The image format (e.g., 'png', 'jpeg', 'gif', 'webp')
    - `<options>`: Optional parameters, separated by semicolons. Currently, supports:
        - `base64`: Indicates the data is base64-encoded (recommended)
    - `<data>`: The image data, either as raw bytes or base64-encoded string

Typical usage with Image widget
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The most common way to use data URIs is with the :class:`~kivy.uix.image.Image` widget,
which handles positioning, scaling, and other display logic::

    from kivy.uix.image import Image

    # Load a base64-encoded PNG image
    data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA..."
    img = Image(source=data_uri)

In KV language::

    Image:
        source: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...'

Advanced usage with CoreImage
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For custom canvas instructions or more control over texture rendering,
you can use CoreImage directly::

    from kivy.core.image import Image as CoreImage

    # Load and get the texture
    data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA..."
    core_img = CoreImage(data_uri)
    texture = core_img.texture

In KV language for custom widgets::

    <MyWidget>:
        canvas:
            Rectangle:
                pos: self.pos
                size: self.size
                texture: CoreImage('data:image/png;base64,iVBORw0KGg...').texture

**Important notes:**

- Images loaded from data URIs are automatically set to `nocache=True` and will not be
cached
- The image type must be supported by one of Kivy's image providers
(png, jpeg, gif, etc.)
- Base64 encoding is recommended for binary image formats (PNG, JPEG, etc.)
- For large images, consider using the BytesIO method instead for better memory
efficiency

Converting a file to a data URI
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Here's how to convert an existing image file to a data URI::

    import base64
    from kivy.uix.image import Image

    # Read and encode an image file
    with open("image.png", "rb") as f:
        encoded = base64.b64encode(f.read()).decode('utf-8')

    # Create data URI and load
    data_uri = f"data:image/png;base64,{encoded}"
    img = Image(source=data_uri)


Saving an image
---------------

A CoreImage can be saved to a file::

    from kivy.core.image import Image as CoreImage
    image = CoreImage(...)
    image.save("/tmp/test.png")

Or you can get the bytes (new in `1.11.0`):

    import io
    from kivy.core.image import Image as CoreImage
    data = io.BytesIO()
    image = CoreImage(...)
    image.save(data, fmt="png")
    png_bytes = data.read()

Provider selection
------------------

.. versionadded:: 3.0.0

By default, Kivy automatically selects an image provider based on platform
defaults and file type. You can override this to use a specific provider.

Querying available providers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To see which providers are available on your system::

    from kivy.core.image import Image as CoreImage
    print(CoreImage.available_providers())  # e.g., ['sdl3', 'pil', 'imageio']

Using ``image_provider`` parameter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Specify a provider when loading an image::

    from kivy.core.image import Image as CoreImage

    # Load with PIL provider
    img = CoreImage.load('photo.jpg', image_provider='pil')

    # Or when creating an Image directly
    img = CoreImage('photo.jpg', image_provider='sdl3')

Provider URI scheme
~~~~~~~~~~~~~~~~~~~

For graphics instructions (Rectangle, BorderImage, etc.) where you cannot pass
extra parameters, use the ``@image_provider:`` URI scheme::

    @image_provider:providername(path/to/image.ext)

An optional parameter block in square brackets can appear between the provider
name and the path. Parameters are ``key=value`` pairs separated by commas::

    @image_provider:providername[key=value,key2=value2](path/to/image.ext)

The only parameter currently defined is ``size``, which is recognised by the
ThorVG SVG provider (``thorvg_svg``) and sets the rasterization size in pixels,
overriding both the SVG's native size and the ``[svg] default_size`` config
setting::

    @image_provider:thorvg_svg[size=256](icons/house.svg)

Example in Python::

    from kivy.graphics import Rectangle

    with widget.canvas:
        Rectangle(source='@image_provider:pil(images/photo.png)')

    # SVG rasterized with 128 px minimum floor (scales up if native size is
    # smaller; leaves native size unchanged if it already exceeds 128)
    with widget.canvas:
        Rectangle(source='@image_provider:thorvg_svg[size=128](icons/house.svg)')

Example in KV language::

    <MyWidget>:
        canvas:
            Rectangle:
                source: '@image_provider:sdl3(images/background.png)'
                pos: self.pos
                size: self.size

The path inside the parentheses is resolved using :func:`~kivy.resources.resource_find`.

SVG images and mipmap
~~~~~~~~~~~~~~~~~~~~~

The ``@image_provider:`` URI controls rasterization size but not mipmap
generation. To create an SVG texture with mipmaps for use in canvas
instructions, load it explicitly via :class:`~kivy.core.image.Image`::

    from kivy.core.image import Image as CoreImage
    from kivy.graphics import Rectangle

    tex = CoreImage('icons/house.svg', mipmap=True).texture

    with widget.canvas:
        Rectangle(texture=tex, pos=self.pos, size=self.size)

The texture is cached (keyed by filename and mipmap flag), so repeated calls
with the same arguments return the cached result without re-rasterizing.

Strict mode
~~~~~~~~~~~

By default, if a requested provider is unavailable or fails, Kivy logs a warning
and falls back to other providers. Enable strict mode to raise exceptions instead::

    import os
    os.environ['KIVY_PROVIDER_STRICT'] = '1'
    import kivy

In strict mode:

- Invalid provider names raise ``ValueError``
- Provider load failures raise ``Exception``
- No fallback to other providers occurs

This is useful during development to catch configuration errors immediately.

Zip file image sequences
~~~~~~~~~~~~~~~~~~~~~~~~

Kivy can load multiple images from a zip file as an animated sequence. When you
load a zip file, all images inside are extracted and combined into a single
Image object with multiple frames::

    from kivy.core.image import Image as CoreImage
    anim = CoreImage('sprites.zip')

If you specify an ``image_provider``, it will be applied to **all** images
within the zip archive::

    # All images in the zip will be loaded using PIL
    anim = CoreImage('sprites.zip', image_provider='pil')

Note that in strict mode, if the specified provider cannot load any image in
the zip, a ``ValueError`` will be raised immediately.

'''
import os
import re
from base64 import b64decode
from filetype import guess_extension

__all__ = ('Image', 'ImageLoader', 'ImageData')

# Regex for @image_provider:providername[optional_params](path) URI scheme.
# Groups: (provider_name, params_string_or_None, path)
_provider_uri_re = re.compile(
    r'^@image_provider:(\w+)(?:\[([^\]]*)\])?\((.+)\)$'
)


def _parse_uri_params(params_str):
    """Parse a URI parameter string into a dict of typed values.

    Accepts a comma-separated list of ``key=value`` pairs, e.g.
    ``"size=512"`` → ``{"size": 512}``.  Integer-looking values are
    converted to ``int``; all others remain strings.  Returns an empty
    dict for *None* or empty input.
    """
    if not params_str:
        return {}
    result = {}
    for token in params_str.split(','):
        token = token.strip()
        if '=' not in token:
            continue
        key, _, val = token.partition('=')
        key = key.strip()
        val = val.strip()
        try:
            result[key] = int(val)
        except ValueError:
            result[key] = val
    return result

from kivy.event import EventDispatcher
from kivy.core import core_register_libs, load_with_provider_selection, \
    get_provider_modules, make_provider_tuple
from kivy.logger import Logger
from kivy.cache import Cache
from kivy.clock import Clock
from kivy.atlas import Atlas
from kivy.resources import resource_find
from kivy.utils import platform
from kivy.setupconfig import USE_SDL3
import zipfile
from io import BytesIO

# late binding
Texture = TextureRegion = None

# register image caching only for keep_data=True
Cache.register('kv.image', timeout=60)
Cache.register('kv.atlas')


[docs] class ImageData(object): '''Container for images and mipmap images. The container will always have at least the mipmap level 0. ''' __slots__ = ('fmt', 'mipmaps', 'source', 'flip_vertical', 'source_image') _supported_fmts = ('rgb', 'bgr', 'rgba', 'bgra', 'argb', 'abgr', 's3tc_dxt1', 's3tc_dxt3', 's3tc_dxt5', 'pvrtc_rgb2', 'pvrtc_rgb4', 'pvrtc_rgba2', 'pvrtc_rgba4', 'etc1_rgb8') def __init__(self, width, height, fmt, data, source=None, flip_vertical=True, source_image=None, rowlength=0): assert fmt in ImageData._supported_fmts #: Decoded image format, one of a available texture format self.fmt = fmt #: Data for each mipmap. self.mipmaps = {} self.add_mipmap(0, width, height, data, rowlength) #: Image source, if available self.source = source #: Indicate if the texture will need to be vertically flipped self.flip_vertical = flip_vertical # the original image, which we might need to save if it is a memoryview self.source_image = source_image def release_data(self): mm = self.mipmaps for item in mm.values(): item[2] = None self.source_image = None @property def width(self): '''Image width in pixels. (If the image is mipmapped, it will use the level 0) ''' return self.mipmaps[0][0] @property def height(self): '''Image height in pixels. (If the image is mipmapped, it will use the level 0) ''' return self.mipmaps[0][1] @property def data(self): '''Image data. (If the image is mipmapped, it will use the level 0) ''' return self.mipmaps[0][2] @property def rowlength(self): '''Image rowlength. (If the image is mipmapped, it will use the level 0) .. versionadded:: 1.9.0 ''' return self.mipmaps[0][3] @property def size(self): '''Image (width, height) in pixels. (If the image is mipmapped, it will use the level 0) ''' mm = self.mipmaps[0] return mm[0], mm[1] @property def have_mipmap(self): return len(self.mipmaps) > 1 def __repr__(self): return ('<ImageData width=%d height=%d fmt=%s ' 'source=%r with %d images>' % ( self.width, self.height, self.fmt, self.source, len(self.mipmaps)))
[docs] def add_mipmap(self, level, width, height, data, rowlength): '''Add a image for a specific mipmap level. .. versionadded:: 1.0.7 ''' self.mipmaps[level] = [int(width), int(height), data, rowlength]
[docs] def get_mipmap(self, level): '''Get the mipmap image at a specific level if it exists .. versionadded:: 1.0.7 ''' if level == 0: return self.width, self.height, self.data, self.rowlength assert level < len(self.mipmaps) return self.mipmaps[level]
[docs] def iterate_mipmaps(self): '''Iterate over all mipmap images available. .. versionadded:: 1.0.7 ''' mm = self.mipmaps for x in range(len(mm)): item = mm.get(x, None) if item is None: raise Exception('Invalid mipmap level, found empty one') yield x, item[0], item[1], item[2], item[3]
class ImageLoaderBase(object): '''Base to implement an image loader.''' _provider_name = None # Internal provider name used for registration. # This must be set by provider implementations. Use # Image.available_providers() to query available provider names. __slots__ = ('_texture', '_data', 'filename', 'keep_data', '_mipmap', '_nocache', '_ext', '_inline') def __init__(self, filename, **kwargs): self._mipmap = kwargs.get('mipmap', False) self.keep_data = kwargs.get('keep_data', False) self._nocache = kwargs.get('nocache', False) self._ext = kwargs.get('ext') self._inline = kwargs.get('inline') self.filename = filename if self._inline: self._data = self.load(kwargs.get('rawdata')) else: self._data = self.load(filename) self._textures = None def load(self, filename): '''Load an image''' return None @staticmethod def can_save(fmt, is_bytesio_like=False): '''Indicate if the loader can save the Image object .. versionchanged:: 1.11.0 Parameter `fmt` and `is_bytesio_like` added ''' return False @staticmethod def can_load_memory(): '''Indicate if the loader can load an image by passing data ''' return False @staticmethod def save(*largs, **kwargs): raise NotImplementedError() def populate(self): self._textures = [] fname = self.filename if __debug__: Logger.trace('Image: %r, populate to textures (%d)' % (fname, len(self._data))) for count in range(len(self._data)): # first, check if a texture with the same name already exist in the # cache uid = f'{fname}|{self._mipmap:d}|{count:d}' texture = Cache.get('kv.texture', uid) # if not create it and append to the cache if texture is None: imagedata = self._data[count] source = (f"{'zip|' if fname.endswith('.zip') else ''}" f"{self._nocache}|") imagedata.source = f'{source}{uid}' texture = Texture.create_from_data( imagedata, mipmap=self._mipmap) if not self._nocache: Cache.append('kv.texture', uid, texture) if imagedata.flip_vertical: texture.flip_vertical() # set as our current texture self._textures.append(texture) # release data if ask if not self.keep_data: self._data[count].release_data() @property def width(self): '''Image width ''' return self._data[0].width @property def height(self): '''Image height ''' return self._data[0].height @property def size(self): '''Image size (width, height) ''' return self._data[0].width, self._data[0].height @property def texture(self): '''Get the image texture (created on the first call) ''' if self._textures is None: self.populate() if self._textures is None: return None return self._textures[0] @property def textures(self): '''Get the textures list (for mipmapped image or animated image) .. versionadded:: 1.0.8 ''' if self._textures is None: self.populate() return self._textures @property def nocache(self): '''Indicate if the texture will not be stored in the cache .. versionadded:: 1.6.0 ''' return self._nocache class ImageLoader(object): loaders = [] loaders_by_name = {} @staticmethod def zip_loader(filename, **kwargs): '''Read images from an zip file. .. versionadded:: 1.0.8 Returns an Image with a list of type ImageData stored in Image._data ''' # extract requested provider (if any) image_provider = kwargs.pop('image_provider', None) # read zip in memory for faster access _file = BytesIO(open(filename, 'rb').read()) # read all images inside the zip z = zipfile.ZipFile(_file) image_data = [] # sort filename list znamelist = z.namelist() znamelist.sort() image = None for zfilename in znamelist: try: # read file and store it in mem with fileIO struct around it tmpfile = BytesIO(z.read(zfilename)) ext = zfilename.split('.')[-1].lower() im = ImageLoader._load_single( zfilename, ext, image_provider=image_provider, rawdata=tmpfile, inline=True, require_memory=True, **kwargs ) if im is not None: # append ImageData to local variable before its # overwritten image_data.append(im._data[0]) image = im # else: if not image file skip to next except ValueError: # Re-raise ValueError (from strict mode provider validation) raise except Exception: Logger.warning('Image: Unable to load image' '<%s> in zip <%s> trying to continue...' % (zfilename, filename)) z.close() if len(image_data) == 0: raise Exception('no images in zip <%s>' % filename) # replace Image.Data with the array of all the images in the zip image._data = image_data image.filename = filename return image @staticmethod def register(defcls): # Require explicit _provider_name attribute (validate BEFORE adding to list) name = getattr(defcls, '_provider_name', None) if name is None: raise ValueError( f'{defcls.__name__} must define a _provider_name class attribute' ) ImageLoader.loaders.append(defcls) ImageLoader.loaders_by_name[name.lower()] = defcls @staticmethod def _load_single(filename, ext, image_provider=None, rawdata=None, inline=False, require_memory=False, **kwargs): # (internal) Load a single image with provider selection. # This is the core loading logic shared by load() and zip_loader(). # :param filename: The filename (used for logging and cache keys) # :param ext: The file extension (e.g., 'png', 'jpg') # :param image_provider: Optional specific provider to use # :param rawdata: Optional BytesIO for in-memory loading # :param inline: Whether this is an inline/memory load # :param require_memory: If True, only use loaders that support memory loading # :param kwargs: Additional arguments passed to the loader # :returns: Loaded image or None if all loaders fail (in lenient mode) # :raises ValueError: If provider not found or doesn't support format # (strict mode) # :raises Exception: If provider fails to load (strict mode) def check_compatibility(loader, extension): """Check if loader can handle this file.""" if extension not in loader.extensions(): return False if require_memory and not loader.can_load_memory(): return False return True def try_load(loader, fname): """Try to load image with the given loader.""" try: Logger.debug(f'Image{loader.__name__[11:]}: Load <{fname}>') if inline and rawdata is not None: return loader(fname, ext=ext, rawdata=rawdata, inline=True, **kwargs) else: return loader(fname, **kwargs) except Exception as e: Logger.warning( f'Image{loader.__name__[11:]}: Failed to load ' f'<{fname}>: {e}' ) return None def fallback_load(): """Load using default priority order.""" for loader in ImageLoader.loaders: if not check_compatibility(loader, ext): continue result = try_load(loader, filename) if result is not None: return result Logger.warning(f'Image: Unknown <{ext}> type, no loader found.') return None return load_with_provider_selection( filename=filename, extension=ext, provider_name=image_provider, providers_by_name=ImageLoader.loaders_by_name, category_name='Image', check_compatibility=check_compatibility, try_load=try_load, fallback_load=fallback_load ) @staticmethod def _load_atlas(filename): # Load an image from an atlas:// URI. # :param filename: The atlas URI (e.g., 'atlas://path/to/atlas/id') # :returns: Image if this is an atlas URI, None otherwise # :raises ValueError: If the atlas URI format is invalid # :raises Exception: If the atlas file cannot be found if not filename.startswith('atlas://'): return None # remove the url prefix rfn = filename[8:] # last field is the ID try: rfn, uid = rfn.rsplit('/', 1) except ValueError: raise ValueError( 'Image: Invalid %s name for atlas' % filename) # search if we already got the atlas loaded atlas = Cache.get('kv.atlas', rfn) # atlas already loaded, so reupload the missing texture in cache, # because when it's not in use, the texture can be removed from the # kv.texture cache. if atlas: texture = atlas[uid] fn = f'atlas://{rfn}/{uid}' cid = f'{fn}|0|0' Cache.append('kv.texture', cid, texture) return Image(texture) # search with resource afn = rfn if not afn.endswith('.atlas'): afn += '.atlas' afn = resource_find(afn) if not afn: raise Exception('Unable to find %r atlas' % afn) atlas = Atlas(afn) Cache.append('kv.atlas', rfn, atlas) # first time, fill our texture cache. for nid, texture in atlas.textures.items(): fn = f'atlas://{rfn}/{nid}' cid = f'{fn}|0|0' Cache.append('kv.texture', cid, texture) return Image(atlas[uid]) @staticmethod def load(filename, **kwargs): # Handle atlas:// URIs result = ImageLoader._load_atlas(filename) if result is not None: return result image_provider = kwargs.pop('image_provider', None) # Handle @image_provider:providername[params](path) URI scheme. # Must be checked BEFORE resource_find so the URI is not passed to it. provider_match = _provider_uri_re.match(filename) if provider_match: # Groups: (provider, optional_params, path) image_provider = provider_match.group(1) uri_params = _parse_uri_params(provider_match.group(2)) inner_path = provider_match.group(3) # Resolve the inner path, then derive extension from it. filename = resource_find(inner_path) or inner_path ext = filename.split('.')[-1].lower() # Pass recognised URI params as loader kwargs (provider-specific). if 'size' in uri_params: kwargs.setdefault('render_size', uri_params['size']) else: # extract extensions ext = filename.split('.')[-1].lower() # prevent url querystrings if filename.startswith(('http://', 'https://')): ext = ext.split('?')[0] filename = resource_find(filename) # Get actual image format instead of extension if possible ext = guess_extension(filename) or ext # special case. When we are trying to load a "zip" file with image, we # will use the special zip_loader in ImageLoader. This might return a # sequence of images contained in the zip. if ext == 'zip': return ImageLoader.zip_loader( filename, image_provider=image_provider, **kwargs ) return ImageLoader._load_single( filename, ext, image_provider=image_provider, **kwargs )
[docs] class Image(EventDispatcher): '''Load an image and store the size and texture. .. versionchanged:: 1.0.7 `mipmap` attribute has been added. The `texture_mipmap` and `texture_rectangle` have been deleted. .. versionchanged:: 1.0.8 An Image widget can change its texture. A new event 'on_texture' has been introduced. New methods for handling sequenced animation have been added. :Parameters: `arg`: can be a string (str), Texture, BytesIO or Image object A string path to the image file or data URI to be loaded; or a Texture object, which will be wrapped in an Image object; or a BytesIO object containing raw image data; or an already existing image object, in which case, a real copy of the given image object will be returned. `keep_data`: bool, defaults to False Keep the image data when the texture is created. `mipmap`: bool, defaults to False Create mipmap for the texture. `anim_delay`: float, defaults to .25 Delay in seconds between each animation frame. Lower values means faster animation. `ext`: str, only with BytesIO `arg` File extension to use in determining how to load raw image data. `filename`: str, only with BytesIO `arg` Filename to use in the image cache for raw image data. `image_provider`: str, defaults to None Name of the image provider to use for loading (e.g., 'sdl3', 'pil'). If None, the default provider selection order is used. Use :meth:`available_providers` to get a list of valid provider names. .. versionadded:: 3.0.0 ''' copy_attributes = ('_size', '_filename', '_texture', '_image', '_mipmap', '_nocache') data_uri_re = re.compile(r'^data:image/([^;,]*)(;[^,]*)?,(.*)$') _anim_ev = None def __init__(self, arg, **kwargs): # this event should be fired on animation of sequenced img's self.register_event_type('on_texture') super(Image, self).__init__() self._mipmap = kwargs.get('mipmap', False) self._keep_data = kwargs.get('keep_data', False) self._nocache = kwargs.get('nocache', False) self._size = [0, 0] self._image = None self._filename = None self._texture = None self._anim_available = False self._anim_index = 0 self._anim_delay = 0 self.anim_delay = kwargs.get('anim_delay', .25) # indicator of images having been loded in cache self._iteration_done = False self._image_provider = kwargs.get('image_provider', None) if isinstance(arg, Image): for attr in Image.copy_attributes: self.__setattr__(attr, arg.__getattribute__(attr)) elif type(arg) in (Texture, TextureRegion): if not hasattr(self, 'textures'): self.textures = [] self.textures.append(arg) self._texture = arg self._size = self.texture.size elif isinstance(arg, ImageLoaderBase): self.image = arg elif isinstance(arg, BytesIO): ext = kwargs.get('ext', None) if not ext: raise Exception('Inline loading require "ext" parameter') filename = kwargs.get('filename') if not filename: self._nocache = True filename = '__inline__' self.load_memory(arg, ext, filename) elif isinstance(arg, str): groups = self.data_uri_re.findall(arg) if groups: self._nocache = True imtype, optstr, data = groups[0] options = [o for o in optstr.split(';') if o] ext = imtype isb64 = 'base64' in options if data: if isb64: data = b64decode(data) self.load_memory(BytesIO(data), ext) else: self.filename = arg else: raise Exception('Unable to load image type {0!r}'.format(arg))
[docs] def remove_from_cache(self): '''Remove the Image from cache. This facilitates re-loading of images from disk in case the image content has changed. .. versionadded:: 1.3.0 Usage:: im = CoreImage('1.jpg') # -- do something -- im.remove_from_cache() im = CoreImage('1.jpg') # this time image will be re-loaded from disk ''' count = 0 uid = f'{self.filename}|{self._mipmap:d}|{count:d}' Cache.remove("kv.image", uid) while Cache.get("kv.texture", uid): Cache.remove("kv.texture", uid) count += 1 uid = f'{self.filename}|{self._mipmap:d}|{count:d}'
def _anim(self, *largs): if not self._image: return textures = self.image.textures if self._anim_index >= len(textures): self._anim_index = 0 self._texture = self.image.textures[self._anim_index] self.dispatch('on_texture') self._anim_index += 1 self._anim_index %= len(self._image.textures)
[docs] def anim_reset(self, allow_anim): '''Reset an animation if available. .. versionadded:: 1.0.8 :Parameters: `allow_anim`: bool Indicate whether the animation should restart playing or not. Usage:: # start/reset animation image.anim_reset(True) # or stop the animation image.anim_reset(False) You can change the animation speed whilst it is playing:: # Set to 20 FPS image.anim_delay = 1 / 20. ''' # stop animation if self._anim_ev is not None: self._anim_ev.cancel() self._anim_ev = None if allow_anim and self._anim_available and self._anim_delay >= 0: self._anim_ev = Clock.schedule_interval(self._anim, self.anim_delay) self._anim()
def _get_anim_delay(self): return self._anim_delay def _set_anim_delay(self, x): if self._anim_delay == x: return self._anim_delay = x if self._anim_available: if self._anim_ev is not None: self._anim_ev.cancel() self._anim_ev = None if self._anim_delay >= 0: self._anim_ev = Clock.schedule_interval(self._anim, self._anim_delay) anim_delay = property(_get_anim_delay, _set_anim_delay) '''Delay between each animation frame. A lower value means faster animation. .. versionadded:: 1.0.8 ''' @property def anim_available(self): '''Return True if this Image instance has animation available. .. versionadded:: 1.0.8 ''' return self._anim_available @property def anim_index(self): '''Return the index number of the image currently in the texture. .. versionadded:: 1.0.8 ''' return self._anim_index def _img_iterate(self, *largs): if not self.image or self._iteration_done: return self._iteration_done = True imgcount = len(self.image.textures) if imgcount > 1: self._anim_available = True self.anim_reset(True) self._texture = self.image.textures[0]
[docs] def on_texture(self, *largs): '''This event is fired when the texture reference or content has changed. It is normally used for sequenced images. .. versionadded:: 1.0.8 ''' pass
[docs] @staticmethod def load(filename, **kwargs): '''Load an image :Parameters: `filename`: str Filename of the image. `keep_data`: bool, defaults to False Keep the image data when the texture is created. ''' kwargs.setdefault('keep_data', False) return Image(filename, **kwargs)
[docs] @staticmethod def available_providers(): '''Return a list of available image provider names. .. versionadded:: 3.0.0 :Returns: list: A list of strings representing the names of available image providers (e.g., ['sdl3', 'pil']). Usage:: from kivy.core.image import Image as CoreImage providers = CoreImage.available_providers() print(providers) # e.g., ['sdl3', 'pil'] ''' return list(ImageLoader.loaders_by_name.keys())
def _get_image(self): return self._image def _set_image(self, image): self._image = image if hasattr(image, 'filename'): self._filename = image.filename if image: self._size = (self.image.width, self.image.height) image = property(_get_image, _set_image, doc='Get/set the data image object') def _get_filename(self): return self._filename def _set_filename(self, value): if value is None or value == self._filename: return self._filename = value # construct uid as a key for Cache f = self.filename uid = f'{f}|{self._mipmap:d}|0' # in case of Image have been asked with keep_data # check the kv.image cache instead of texture. image = Cache.get('kv.image', uid) if image: # we found an image, yeah ! but reset the texture now. self.image = image # if image.__class__ is core image then it's a texture # from atlas or other sources and has no data so skip if (image.__class__ != self.__class__ and not image.keep_data and self._keep_data): self.remove_from_cache() self._filename = '' self._set_filename(value) else: self._texture = None return else: # if we already got a texture, it will be automatically reloaded. _texture = Cache.get('kv.texture', uid) if _texture: self._texture = _texture return # if image not already in cache then load tmpfilename = self._filename image = ImageLoader.load( self._filename, keep_data=self._keep_data, mipmap=self._mipmap, nocache=self._nocache, image_provider=self._image_provider) self._filename = tmpfilename # put the image into the cache if needed if isinstance(image, Texture): self._texture = image self._size = image.size else: self.image = image if not self._nocache: Cache.append('kv.image', uid, self.image) filename = property(_get_filename, _set_filename, doc='Get/set the filename of image')
[docs] def load_memory(self, data, ext, filename='__inline__'): '''(internal) Method to load an image from raw data. ''' self._filename = filename image = ImageLoader._load_single( filename, ext, image_provider=self._image_provider, rawdata=data, inline=True, require_memory=True, nocache=self._nocache, mipmap=self._mipmap, keep_data=self._keep_data ) if image is None: raise Exception(f'No inline loader found to load {ext}') if isinstance(image, Texture): self._texture = image self._size = image.size else: self.image = image
@property def size(self): '''Image size (width, height) ''' return self._size @property def width(self): '''Image width ''' return self._size[0] @property def height(self): '''Image height ''' return self._size[1] @property def texture(self): '''Texture of the image''' if self.image: if not self._iteration_done: self._img_iterate() return self._texture @property def nocache(self): '''Indicate whether the texture will not be stored in the cache or not. .. versionadded:: 1.6.0 ''' return self._nocache
[docs] def save(self, filename, flipped=False, fmt=None): '''Save image texture to file. The filename should have the '.png' extension because the texture data read from the GPU is in the RGBA format. '.jpg' might work but has not been heavily tested so some providers might break when using it. Any other extensions are not officially supported. The flipped parameter flips the saved image vertically, and defaults to False. Example:: # Save an core image object from kivy.core.image import Image img = Image('hello.png') img.save('hello2.png') # Save a texture texture = Texture.create(...) img = Image(texture) img.save('hello3.png') .. versionadded:: 1.7.0 .. versionchanged:: 1.8.0 Parameter `flipped` added to flip the image before saving, default to False. .. versionchanged:: 1.11.0 Parameter `fmt` added to force the output format of the file Filename can now be a BytesIO object. ''' is_bytesio_like = False if callable(getattr(filename, 'read', None)): # BytesIO like(RawIO, RawIOBase, BytesIO..) is_bytesio_like = True if not fmt: raise Exception( "You must specify a format to save into a BytesIO like object") elif fmt is None: fmt = self._find_format_from_filename(filename) pixels = None size = None loaders = [ x for x in ImageLoader.loaders if x.can_save(fmt, is_bytesio_like=is_bytesio_like) ] if not loaders: return False loader = loaders[0] if self.image: # we might have a ImageData object to use data = self.image._data[0] if data.data is not None: if data.fmt in ('rgba', 'rgb'): # fast path, use the "raw" data when keep_data is used size = data.width, data.height pixels = data.data else: # the format is not rgba, we need to convert it. # use texture for that. self.populate() if pixels is None and self._texture: # use the texture pixels size = self._texture.size pixels = self._texture.pixels if pixels is None: return False l_pixels = len(pixels) if l_pixels == size[0] * size[1] * 3: pixelfmt = 'rgb' elif l_pixels == size[0] * size[1] * 4: pixelfmt = 'rgba' else: raise Exception('Unable to determine the format of the pixels') return loader.save( filename, size[0], size[1], pixelfmt, pixels, flipped, fmt)
def _find_format_from_filename(self, filename): ext = filename.rsplit(".", 1)[-1].lower() if (ext in {'bmp', 'jpe', 'lbm', 'pcx', 'png', 'pnm', 'tga', 'tiff', 'webp', 'xcf', 'xpm', 'xv'}): return ext elif ext in ('jpg', 'jpeg'): return 'jpg' elif ext in ('b64', 'base64'): return 'base64' return None
[docs] def read_pixel(self, x, y): '''For a given local x/y position, return the pixel color at that position. .. warning:: This function can only be used with images loaded with the keep_data=True keyword. For example:: m = Image.load('image.png', keep_data=True) color = m.read_pixel(150, 150) :Parameters: `x`: int Local x coordinate of the pixel in question. `y`: int Local y coordinate of the pixel in question. ''' data = self.image._data[0] # can't use this function without ImageData if data.data is None: raise EOFError('Image data is missing, make sure that image is' 'loaded with keep_data=True keyword.') # check bounds x, y = int(x), int(y) if not (0 <= x < data.width and 0 <= y < data.height): raise IndexError(f'Position ({x:d}, {y:d}) is out of range.') assert data.fmt in ImageData._supported_fmts size = 3 if data.fmt in ('rgb', 'bgr') else 4 index = y * data.width * size + x * size raw = bytearray(data.data[index:index + size]) color = [c / 255.0 for c in raw] bgr_flag = False if data.fmt == 'argb': color.reverse() # bgra bgr_flag = True elif data.fmt == 'abgr': color.reverse() # rgba # conversion for BGR->RGB, BGRA->RGBA format if bgr_flag or data.fmt in ('bgr', 'bgra'): color[0], color[2] = color[2], color[0] return color
def load(filename): '''Load an image''' return Image.load(filename) # load image loaders # Build platform-specific list from registry all_providers = get_provider_modules('image') image_libs = [] if platform in ('macosx', 'ios'): image_libs.append(make_provider_tuple('imageio', all_providers)) image_libs.append(make_provider_tuple('tex', all_providers)) image_libs.append(make_provider_tuple('dds', all_providers)) if USE_SDL3: image_libs.append(make_provider_tuple('sdl3', all_providers)) image_libs.append(make_provider_tuple('ffpy', all_providers)) image_libs.append(make_provider_tuple('pil', all_providers)) image_libs.append(make_provider_tuple('thorvg_svg', all_providers)) libs_loaded = core_register_libs('image', image_libs) from os import environ if 'KIVY_DOC' not in environ and not libs_loaded: import sys Logger.critical('App: Unable to get any Image provider, abort.') sys.exit(1) # resolve binding. from kivy.graphics.texture import Texture, TextureRegion # noqa: F811