Repository: kivy/kivy

Issue:
`imghdr` is deprecated and slated for removal in Python 3.13
We currently make use of the `imghdr` module to guess the filetype with our `ImageLoader`.

See: https://peps.python.org/pep-0594/#imghdr


Discussion:
Is this issue up for grab? Id like to contribute to this! 
> Is this issue up for grab? Id like to contribute to this!

Yes. Is still available. (And I now assigned it to you)

Thank you!
Thank you so much! id like to contribute with another person. 
It would be great if you could assign it to them too! 

Their github link : https://github.com/vsh9 
@bharathan06 I don't think this issue needs two people to solve :thinking:,  `imghdr` is only used to find the extension of the image file, 
see https://github.com/kivy/kivy/blob/ca1b918c656f23e401707388f25f4a63d9b8ae7d/kivy/core/image/__init__.py#L452-L461

I propose using [`filetype`](https://pypi.org/project/filetype/) as it supports a lot of file types 
> I propose using [filetype](https://pypi.org/project/filetype/) as it supports a lot of file types

I think it's better to use magic number to detect a subset of the fileformat ( same method used by imghdr )
> > I propose using [filetype](https://pypi.org/project/filetype/) as it supports a lot of file types
> 
> I think it's better to use magic number to detect a subset of the fileformat ( same method used by imghdr )

from filetype docs
> Small and dependency-free [Python](http://python.org/) package to infer file type and MIME type checking the [magic numbers](https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files) signature of a file or buffer.
@bharathan06: Just checking in. How did you go with this?
I'm considering @bharathan06 not available anymore to take care of it, the issue is now in 3.0.0 roadmap and free to pick.
@misl6 i want to take on it. 
That's something lovely to hear @kengoon !

Assigned the issue. Please bear in mind that we're in `3.0.0`, so don't be afraid to do any refactoring / cleanup if needed.


Resolving patch:
diff --git a/kivy/core/image/__init__.py b/kivy/core/image/__init__.py
--- a/kivy/core/image/__init__.py
+++ b/kivy/core/image/__init__.py
@@ -62,7 +62,7 @@
 '''
 import re
 from base64 import b64decode
-import imghdr
+from filetype import guess_extension
 
 __all__ = ('Image', 'ImageLoader', 'ImageData')
 
@@ -79,11 +79,9 @@
 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')
@@ -187,7 +185,7 @@ def get_mipmap(self, level):
         .. versionadded:: 1.0.7
         '''
         if level == 0:
-            return (self.width, self.height, self.data, self.rowlength)
+            return self.width, self.height, self.data, self.rowlength
         assert level < len(self.mipmaps)
         return self.mipmaps[level]
 
@@ -257,17 +255,15 @@ def populate(self):
 
             # first, check if a texture with the same name already exist in the
             # cache
-            chr = type(fname)
-            uid = chr(u'%s|%d|%d') % (fname, self._mipmap, count)
+            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 = '{}{}|'.format(
-                    'zip|' if fname.endswith('.zip') else '',
-                    self._nocache)
-                imagedata.source = chr(source) + uid
+                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:
@@ -298,7 +294,7 @@ def height(self):
     def size(self):
         '''Image size (width, height)
         '''
-        return (self._data[0].width, self._data[0].height)
+        return self._data[0].width, self._data[0].height
 
     @property
     def texture(self):
@@ -330,7 +326,6 @@ def nocache(self):
 
 
 class ImageLoader(object):
-
     loaders = []
 
     @staticmethod
@@ -413,8 +408,8 @@ def load(filename, **kwargs):
             # kv.texture cache.
             if atlas:
                 texture = atlas[uid]
-                fn = 'atlas://%s/%s' % (rfn, uid)
-                cid = '{}|{:d}|{:d}'.format(fn, False, 0)
+                fn = f'atlas://{rfn}/{uid}'
+                cid = f'{fn}|0|0'
                 Cache.append('kv.texture', cid, texture)
                 return Image(texture)
 
@@ -429,8 +424,8 @@ def load(filename, **kwargs):
             Cache.append('kv.atlas', rfn, atlas)
             # first time, fill our texture cache.
             for nid, texture in atlas.textures.items():
-                fn = 'atlas://%s/%s' % (rfn, nid)
-                cid = '{}|{:d}|{:d}'.format(fn, False, 0)
+                fn = f'atlas://{rfn}/{nid}'
+                cid = f'{fn}|0|0'
                 Cache.append('kv.texture', cid, texture)
             return Image(atlas[uid])
 
@@ -438,11 +433,14 @@ def load(filename, **kwargs):
         ext = filename.split('.')[-1].lower()
 
         # prevent url querystrings
-        if filename.startswith((('http://', 'https://'))):
+        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.
@@ -450,17 +448,14 @@ def load(filename, **kwargs):
             return ImageLoader.zip_loader(filename)
         else:
             im = None
-            # Get actual image format instead of extension if possible
-            ext = imghdr.what(filename) or ext
             for loader in ImageLoader.loaders:
                 if ext not in loader.extensions():
                     continue
-                Logger.debug('Image%s: Load <%s>' %
-                             (loader.__name__[11:], filename))
+                Logger.debug(f'Image{loader.__name__[11:]}: Load <{filename}>')
                 im = loader(filename, **kwargs)
                 break
             if im is None:
-                raise Exception('Unknown <%s> type, no loader found.' % ext)
+                raise Exception(f'Unknown <{ext}> type, no loader found.')
             return im
 
 
@@ -578,14 +573,12 @@ def remove_from_cache(self):
 
         '''
         count = 0
-        f = self.filename
-        pat = type(f)(u'%s|%d|%d')
-        uid = pat % (f, self._mipmap, count)
+        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 = pat % (f, self._mipmap, count)
+            uid = f'{self.filename}|{self._mipmap:d}|{count:d}'
 
     def _anim(self, *largs):
         if not self._image:
@@ -724,7 +717,7 @@ def _set_filename(self, value):
 
         # construct uid as a key for Cache
         f = self.filename
-        uid = type(f)(u'%s|%d|%d') % (f, self._mipmap, 0)
+        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.
@@ -777,10 +770,10 @@ def load_memory(self, data, ext, filename='__inline__'):
                    loader.can_load_memory() and
                    ext in loader.extensions()]
         if not loaders:
-            raise Exception('No inline loader found to load {}'.format(ext))
+            raise Exception(f'No inline loader found to load {ext}')
         image = loaders[0](filename, ext=ext, rawdata=data, inline=True,
-                nocache=self._nocache, mipmap=self._mipmap,
-                keep_data=self._keep_data)
+                           nocache=self._nocache, mipmap=self._mipmap,
+                           keep_data=self._keep_data)
         if isinstance(image, Texture):
             self._texture = image
             self._size = image.size
@@ -908,9 +901,9 @@ def save(self, filename, flipped=False, fmt=None):
 
     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'}:
+        if (ext in
+                {'bmp', 'jpe', 'lbm', 'pcx', 'png', 'pnm', 'tga',
+                 'tiff', 'webp', 'xcf', 'xpm', 'xv'}):
             return ext
         elif ext in ('jpg', 'jpeg'):
             return 'jpg'
@@ -945,7 +938,7 @@ def read_pixel(self, x, y):
         # check bounds
         x, y = int(x), int(y)
         if not (0 <= x < data.width and 0 <= y < data.height):
-            raise IndexError('Position (%d, %d) is out of range.' % (x, y))
+            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
@@ -992,6 +985,7 @@ def load(filename):
 libs_loaded = core_register_libs('image', image_libs)
 
 from os import environ
+
 if 'KIVY_DOC' not in environ and not libs_loaded:
     import sys
 


Test patch: