Stav 23.06.2026

This commit is contained in:
2026-06-23 15:20:56 +02:00
commit 6d91e83e8c
5670 changed files with 1145969 additions and 0 deletions
@@ -0,0 +1,247 @@
'''
Core Abstraction
================
This module defines the abstraction layers for our core providers and their
implementations. For further information, please refer to
:ref:`architecture` and the :ref:`providers` section of the documentation.
In most cases, you shouldn't directly use a library that's already covered
by the core abstraction. Always try to use our providers first.
In case we are missing a feature or method, please let us know by
opening a new Bug report instead of relying on your library.
.. warning::
These are **not** widgets! These are just abstractions of the respective
functionality. For example, you cannot add a core image to your window.
You have to use the image **widget** class instead. If you're really
looking for widgets, please refer to :mod:`kivy.uix` instead.
'''
import os
import sysconfig
import sys
import traceback
import tempfile
import subprocess
import importlib
import kivy
from kivy.logger import Logger
class CoreCriticalException(Exception):
pass
def core_select_lib(category, llist, create_instance=False,
base='kivy.core', basemodule=None):
if 'KIVY_DOC' in os.environ:
return
category = category.lower()
basemodule = basemodule or category
libs_ignored = []
errs = []
for option, modulename, classname in llist:
try:
# module activated in config ?
try:
if option not in kivy.kivy_options[category]:
libs_ignored.append(modulename)
Logger.debug(
'{0}: Provider <{1}> ignored by config'.format(
category.capitalize(), option))
continue
except KeyError:
pass
# import module
mod = importlib.__import__(name='{2}.{0}.{1}'.format(
basemodule, modulename, base),
globals=globals(),
locals=locals(),
fromlist=[modulename], level=0)
cls = mod.__getattribute__(classname)
# ok !
Logger.info('{0}: Provider: {1}{2}'.format(
category.capitalize(), option,
'({0} ignored)'.format(libs_ignored) if libs_ignored else ''))
if create_instance:
cls = cls()
return cls
except ImportError as e:
errs.append((option, e, sys.exc_info()[2]))
libs_ignored.append(modulename)
Logger.debug('{0}: Ignored <{1}> (import error)'.format(
category.capitalize(), option))
Logger.trace('', exc_info=e)
except CoreCriticalException as e:
errs.append((option, e, sys.exc_info()[2]))
Logger.error('{0}: Unable to use {1}'.format(
category.capitalize(), option))
Logger.error(
'{0}: The module raised an important error: {1!r}'.format(
category.capitalize(), e.message))
raise
except Exception as e:
errs.append((option, e, sys.exc_info()[2]))
libs_ignored.append(modulename)
Logger.trace('{0}: Unable to use {1}'.format(
category.capitalize(), option))
Logger.trace('', exc_info=e)
err = '\n'.join(['{} - {}: {}\n{}'.format(opt, e.__class__.__name__, e,
''.join(traceback.format_tb(tb))) for opt, e, tb in errs])
Logger.critical(
'{0}: Unable to find any valuable {0} provider. Please enable '
'debug logging (e.g. add -d if running from the command line, or '
'change the log level in the config) and re-run your app to '
'identify potential causes\n{1}'.format(category.capitalize(), err))
def core_register_libs(category, libs, base='kivy.core'):
if 'KIVY_DOC' in os.environ:
return
category = category.lower()
kivy_options = kivy.kivy_options[category]
libs_loadable = {}
libs_ignored = []
for option, lib in libs:
# module activated in config ?
if option not in kivy_options:
Logger.debug('{0}: option <{1}> ignored by config'.format(
category.capitalize(), option))
libs_ignored.append(lib)
continue
libs_loadable[option] = lib
libs_loaded = []
for item in kivy_options:
try:
# import module
try:
lib = libs_loadable[item]
except KeyError:
continue
importlib.__import__(name='{2}.{0}.{1}'.format(category, lib, base),
globals=globals(),
locals=locals(),
fromlist=[lib],
level=0)
libs_loaded.append(lib)
except Exception as e:
Logger.trace('{0}: Unable to use <{1}> as loader!'.format(
category.capitalize(), option))
Logger.trace('', exc_info=e)
libs_ignored.append(lib)
Logger.info('{0}: Providers: {1} {2}'.format(
category.capitalize(),
', '.join(libs_loaded),
'({0} ignored)'.format(
', '.join(libs_ignored)) if libs_ignored else ''))
return libs_loaded
def handle_win_lib_import_error(category, provider, mod_name):
if sys.platform != 'win32':
return
assert mod_name.startswith('kivy.')
kivy_root = os.path.dirname(kivy.__file__)
dirs = mod_name[5:].split('.')
mod_path = os.path.join(kivy_root, *dirs)
# get the full expected path to the compiled pyd file
# filename is <debug>.cp<major><minor>-<platform>.pyd
# https://github.com/python/cpython/blob/master/Doc/whatsnew/3.5.rst
if hasattr(sys, 'gettotalrefcount'): # debug
mod_path += '._d'
mod_path += '.cp{}{}-{}.pyd'.format(
sys.version_info.major, sys.version_info.minor,
sysconfig.get_platform().replace('-', '_'))
# does the compiled pyd exist at all?
if not os.path.exists(mod_path):
Logger.debug(
'{}: Failed trying to import "{}" for provider {}. Compiled file '
'does not exist. Have you perhaps forgotten to compile Kivy, or '
'did not install all required dependencies?'.format(
category, provider, mod_path))
return
# tell user to provide dependency walker
env_var = 'KIVY_{}_DEPENDENCY_WALKER'.format(provider.upper())
if env_var not in os.environ:
Logger.debug(
'{0}: Failed trying to import the "{1}" provider from "{2}". '
'This error is often encountered when a dependency is missing,'
' or if there are multiple copies of the same dependency dll on '
'the Windows PATH and they are incompatible with each other. '
'This can occur if you are mixing installations (such as different'
' python installations, like anaconda python and a system python) '
'or if another unrelated program added its directory to the PATH. '
'Please examine your PATH and python installation for potential '
'issues. To further troubleshoot a "DLL load failed" error, '
'please download '
'"Dependency Walker" (64 or 32 bit version - matching your python '
'bitness) from dependencywalker.com and set the environment '
'variable {3} to the full path of the downloaded depends.exe file '
'and rerun your application to generate an error report'.
format(category, provider, mod_path, env_var))
return
depends_bin = os.environ[env_var]
if not os.path.exists(depends_bin):
raise ValueError('"{}" provided in {} does not exist'.format(
depends_bin, env_var))
# make file for the resultant log
fd, temp_file = tempfile.mkstemp(
suffix='.dwi', prefix='kivy_depends_{}_log_'.format(provider),
dir=os.path.expanduser('~/'))
os.close(fd)
Logger.info(
'{}: Running dependency walker "{}" on "{}" to generate '
'troubleshooting log. Please wait for it to complete'.format(
category, depends_bin, mod_path))
Logger.debug(
'{}: Dependency walker command is "{}"'.format(
category,
[depends_bin, '/c', '/od:{}'.format(temp_file), mod_path]))
try:
subprocess.check_output([
depends_bin, '/c', '/od:{}'.format(temp_file), mod_path])
except subprocess.CalledProcessError as exc:
if exc.returncode >= 0x00010000:
Logger.error(
'{}: Dependency walker failed with error code "{}". No '
'error report was generated'.
format(category, exc.returncode))
return
Logger.info(
'{}: dependency walker generated "{}" containing troubleshooting '
'information about provider {} and its failing file "{} ({})". You '
'can open the file in dependency walker to view any potential issues '
'and troubleshoot it yourself. '
'To share the file with the Kivy developers and request support, '
'please contact us at our support channels '
'https://kivy.org/doc/master/contact.html (not on github, unless '
'it\'s truly a bug). Make sure to provide the generated file as well '
'as the *complete* Kivy log being printed here. Keep in mind the '
'generated dependency walker log file contains paths to dlls on your '
'system used by kivy or its dependencies to help troubleshoot them, '
'and these paths may include your name in them. Please view the '
'log file in dependency walker before sharing to ensure you are not '
'sharing sensitive paths'.format(
category, temp_file, provider, mod_name, mod_path))
@@ -0,0 +1,223 @@
'''
Audio
=====
Load an audio sound and play it with::
from kivy.core.audio import SoundLoader
sound = SoundLoader.load('mytest.wav')
if sound:
print("Sound found at %s" % sound.source)
print("Sound is %.3f seconds" % sound.length)
sound.play()
You should not use the Sound class directly. The class returned by
:func:`SoundLoader.load` will be the best sound provider for that particular
file type, so it might return different Sound classes depending the file type.
Event dispatching and state changes
-----------------------------------
Audio is often processed in parallel to your code. This means you often need to
enter the Kivy :func:`eventloop <kivy.base.EventLoopBase>` in order to allow
events and state changes to be dispatched correctly.
You seldom need to worry about this as Kivy apps typically always
require this event loop for the GUI to remain responsive, but it is good to
keep this in mind when debugging or running in a
`REPL <https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop>`_
(Read-eval-print loop).
.. versionchanged:: 1.10.0
The pygst and gi providers have been removed.
.. versionchanged:: 1.8.0
There are now 2 distinct Gstreamer implementations: one using Gi/Gst
working for both Python 2+3 with Gstreamer 1.0, and one using PyGST
working only for Python 2 + Gstreamer 0.10.
.. note::
The core audio library does not support recording audio. If you require
this functionality, please refer to the
`audiostream <https://github.com/kivy/audiostream>`_ extension.
'''
__all__ = ('Sound', 'SoundLoader')
from kivy.logger import Logger
from kivy.event import EventDispatcher
from kivy.core import core_register_libs
from kivy.resources import resource_find
from kivy.properties import StringProperty, NumericProperty, OptionProperty, \
AliasProperty, BooleanProperty, BoundedNumericProperty
from kivy.utils import platform
from kivy.setupconfig import USE_SDL2
from sys import float_info
class SoundLoader:
'''Load a sound, using the best loader for the given file type.
'''
_classes = []
@staticmethod
def register(classobj):
'''Register a new class to load the sound.'''
Logger.debug('Audio: register %s' % classobj.__name__)
SoundLoader._classes.append(classobj)
@staticmethod
def load(filename):
'''Load a sound, and return a Sound() instance.'''
rfn = resource_find(filename)
if rfn is not None:
filename = rfn
ext = filename.split('.')[-1].lower()
if '?' in ext:
ext = ext.split('?')[0]
for classobj in SoundLoader._classes:
if ext in classobj.extensions():
return classobj(source=filename)
Logger.warning('Audio: Unable to find a loader for <%s>' %
filename)
return None
class Sound(EventDispatcher):
'''Represents a sound to play. This class is abstract, and cannot be used
directly.
Use SoundLoader to load a sound.
:Events:
`on_play`: None
Fired when the sound is played.
`on_stop`: None
Fired when the sound is stopped.
'''
source = StringProperty(None)
'''Filename / source of your audio file.
.. versionadded:: 1.3.0
:attr:`source` is a :class:`~kivy.properties.StringProperty` that defaults
to None and is read-only. Use the :meth:`SoundLoader.load` for loading
audio.
'''
volume = NumericProperty(1.)
'''Volume, in the range 0-1. 1 means full volume, 0 means mute.
.. versionadded:: 1.3.0
:attr:`volume` is a :class:`~kivy.properties.NumericProperty` and defaults
to 1.
'''
pitch = BoundedNumericProperty(1., min=float_info.epsilon)
'''Pitch of a sound. 2 is an octave higher, .5 one below. This is only
implemented for SDL2 audio provider yet.
.. versionadded:: 1.10.0
:attr:`pitch` is a :class:`~kivy.properties.NumericProperty` and defaults
to 1.
'''
state = OptionProperty('stop', options=('stop', 'play'))
'''State of the sound, one of 'stop' or 'play'.
.. versionadded:: 1.3.0
:attr:`state` is a read-only :class:`~kivy.properties.OptionProperty`.'''
loop = BooleanProperty(False)
'''Set to True if the sound should automatically loop when it finishes.
.. versionadded:: 1.8.0
:attr:`loop` is a :class:`~kivy.properties.BooleanProperty` and defaults to
False.'''
__events__ = ('on_play', 'on_stop')
def on_source(self, instance, filename):
self.unload()
if filename is None:
return
self.load()
def get_pos(self):
'''
Returns the current position of the audio file.
Returns 0 if not playing.
.. versionadded:: 1.4.1
'''
return 0
def _get_length(self):
return 0
length = property(lambda self: self._get_length(),
doc='Get length of the sound (in seconds).')
def load(self):
'''Load the file into memory.'''
pass
def unload(self):
'''Unload the file from memory.'''
pass
def play(self):
'''Play the file.'''
self.state = 'play'
self.dispatch('on_play')
def stop(self):
'''Stop playback.'''
self.state = 'stop'
self.dispatch('on_stop')
def seek(self, position):
'''Go to the <position> (in seconds).
.. note::
Most sound providers cannot seek when the audio is stopped.
Play then seek.
'''
pass
def on_play(self):
pass
def on_stop(self):
pass
# Little trick here, don't activate gstreamer on window
# seem to have lot of crackle or something...
audio_libs = []
if platform == 'android':
audio_libs += [('android', 'audio_android')]
elif platform in ('macosx', 'ios'):
audio_libs += [('avplayer', 'audio_avplayer')]
try:
from kivy.lib.gstplayer import GstPlayer # NOQA
audio_libs += [('gstplayer', 'audio_gstplayer')]
except ImportError:
pass
audio_libs += [('ffpyplayer', 'audio_ffpyplayer')]
if USE_SDL2:
audio_libs += [('sdl2', 'audio_sdl2')]
else:
audio_libs += [('pygame', 'audio_pygame')]
libs_loaded = core_register_libs('audio', audio_libs)
@@ -0,0 +1,104 @@
"""
AudioAndroid: Kivy audio implementation for Android using native API
"""
__all__ = ("SoundAndroidPlayer", )
from jnius import autoclass, java_method, PythonJavaClass
from android import api_version
from kivy.core.audio import Sound, SoundLoader
MediaPlayer = autoclass("android.media.MediaPlayer")
AudioManager = autoclass("android.media.AudioManager")
if api_version >= 21:
AudioAttributesBuilder = autoclass("android.media.AudioAttributes$Builder")
class OnCompletionListener(PythonJavaClass):
__javainterfaces__ = ["android/media/MediaPlayer$OnCompletionListener"]
__javacontext__ = "app"
def __init__(self, callback, **kwargs):
super(OnCompletionListener, self).__init__(**kwargs)
self.callback = callback
@java_method("(Landroid/media/MediaPlayer;)V")
def onCompletion(self, mp):
self.callback()
class SoundAndroidPlayer(Sound):
@staticmethod
def extensions():
return ("mp3", "mp4", "aac", "3gp", "flac", "mkv", "wav", "ogg", "m4a",
"gsm", "mid", "xmf", "mxmf", "rtttl", "rtx", "ota", "imy")
def __init__(self, **kwargs):
self._mediaplayer = None
self._completion_listener = None
super(SoundAndroidPlayer, self).__init__(**kwargs)
def load(self):
self.unload()
self._mediaplayer = MediaPlayer()
if api_version >= 21:
self._mediaplayer.setAudioAttributes(
AudioAttributesBuilder()
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.build())
else:
self._mediaplayer.setAudioStreamType(AudioManager.STREAM_MUSIC)
self._mediaplayer.setDataSource(self.source)
self._completion_listener = OnCompletionListener(
self._completion_callback
)
self._mediaplayer.setOnCompletionListener(self._completion_listener)
self._mediaplayer.prepare()
def unload(self):
if self._mediaplayer:
self._mediaplayer.release()
self._mediaplayer = None
def play(self):
if not self._mediaplayer:
return
self._mediaplayer.start()
super(SoundAndroidPlayer, self).play()
def stop(self):
if not self._mediaplayer:
return
self._mediaplayer.stop()
self._mediaplayer.prepare()
def seek(self, position):
if not self._mediaplayer:
return
self._mediaplayer.seekTo(float(position) * 1000)
def get_pos(self):
if self._mediaplayer:
return self._mediaplayer.getCurrentPosition() / 1000.
return super(SoundAndroidPlayer, self).get_pos()
def on_volume(self, instance, volume):
if self._mediaplayer:
volume = float(volume)
self._mediaplayer.setVolume(volume, volume)
def _completion_callback(self):
super(SoundAndroidPlayer, self).stop()
def _get_length(self):
if self._mediaplayer:
return self._mediaplayer.getDuration() / 1000.
return super(SoundAndroidPlayer, self)._get_length()
def on_loop(self, instance, loop):
if self._mediaplayer:
self._mediaplayer.setLooping(loop)
SoundLoader.register(SoundAndroidPlayer)
@@ -0,0 +1,82 @@
'''
AudioAvplayer: implementation of Sound using pyobjus / AVFoundation.
Works on iOS / OSX.
'''
__all__ = ('SoundAvplayer', )
from kivy.core.audio import Sound, SoundLoader
from pyobjus import autoclass, protocol
from pyobjus.dylib_manager import load_framework, INCLUDE
load_framework(INCLUDE.AVFoundation)
AVAudioPlayer = autoclass("AVAudioPlayer")
NSURL = autoclass("NSURL")
NSString = autoclass("NSString")
class SoundAvplayer(Sound):
@staticmethod
def extensions():
# taken from https://goo.gl/015kvU
return ("aac", "adts", "aif", "aiff", "aifc", "caf", "mp3", "mp4",
"m4a", "snd", "au", "sd2", "wav")
def __init__(self, **kwargs):
self._avplayer = None
super(SoundAvplayer, self).__init__(**kwargs)
def load(self):
self.unload()
fn = NSString.alloc().initWithUTF8String_(self.source)
url = NSURL.alloc().initFileURLWithPath_(fn)
self._avplayer = AVAudioPlayer.alloc().initWithContentsOfURL_error_(
url, None)
def unload(self):
self.stop()
self._avplayer = None
def play(self):
if not self._avplayer:
return
self._avplayer.delegate = self
self._avplayer.play()
super(SoundAvplayer, self).play()
def stop(self):
if not self._avplayer:
return
self._avplayer.delegate = None
self._avplayer.stop()
super(SoundAvplayer, self).stop()
def seek(self, position):
if not self._avplayer:
return
avplayer = self._avplayer
avplayer.stop()
avplayer.currentTime = float(position)
if self.state == 'play':
avplayer.play()
def get_pos(self):
if self._avplayer:
return self._avplayer.currentTime
return super(SoundAvplayer, self).get_pos()
def on_volume(self, instance, volume):
if self._avplayer:
self._avplayer.volume = float(volume)
def _get_length(self):
if self._avplayer:
return self._avplayer.duration
return super(SoundAvplayer, self)._get_length()
@protocol("AVAudioPlayerDelegate")
def audioPlayerDidFinishPlaying_successfully_(self, player, flag):
self.stop()
SoundLoader.register(SoundAvplayer)
@@ -0,0 +1,185 @@
'''
FFmpeg based audio player
=========================
To use, you need to install ffpyplayer and have a compiled ffmpeg shared
library.
https://github.com/matham/ffpyplayer
The docs there describe how to set this up. But briefly, first you need to
compile ffmpeg using the shared flags while disabling the static flags (you'll
probably have to set the fPIC flag, e.g. CFLAGS=-fPIC). Here's some
instructions: https://trac.ffmpeg.org/wiki/CompilationGuide. For Windows, you
can download compiled GPL binaries from http://ffmpeg.zeranoe.com/builds/.
Similarly, you should download SDL.
Now, you should a ffmpeg and sdl directory. In each, you should have a include,
bin, and lib directory, where e.g. for Windows, lib contains the .dll.a files,
while bin contains the actual dlls. The include directory holds the headers.
The bin directory is only needed if the shared libraries are not already on
the path. In the environment define FFMPEG_ROOT and SDL_ROOT, each pointing to
the ffmpeg, and SDL directories, respectively. (If you're using SDL2,
the include directory will contain a directory called SDL2, which then holds
the headers).
Once defined, download the ffpyplayer git and run
python setup.py build_ext --inplace
Finally, before running you need to ensure that ffpyplayer is in python's path.
..Note::
When kivy exits by closing the window while the audio is playing,
it appears that the __del__method of SoundFFPy
is not called. Because of this the SoundFFPy object is not
properly deleted when kivy exits. The consequence is that because
MediaPlayer creates internal threads which do not have their daemon
flag set, when the main threads exists it'll hang and wait for the other
MediaPlayer threads to exit. But since __del__ is not called to delete the
MediaPlayer object, those threads will remain alive hanging kivy. What this
means is that you have to be sure to delete the MediaPlayer object before
kivy exits by setting it to None.
'''
__all__ = ('SoundFFPy', )
try:
import ffpyplayer
from ffpyplayer.player import MediaPlayer
from ffpyplayer.tools import set_log_callback, get_log_callback, formats_in
except:
raise
from kivy.clock import Clock
from kivy.logger import Logger
from kivy.core.audio import Sound, SoundLoader
from kivy.weakmethod import WeakMethod
import time
try:
Logger.info(
'SoundFFPy: Using ffpyplayer {}'.format(ffpyplayer.__version__))
except:
Logger.info('SoundFFPy: Using ffpyplayer {}'.format(ffpyplayer.version))
logger_func = {'quiet': Logger.critical, 'panic': Logger.critical,
'fatal': Logger.critical, 'error': Logger.error,
'warning': Logger.warning, 'info': Logger.info,
'verbose': Logger.debug, 'debug': Logger.debug}
def _log_callback(message, level):
message = message.strip()
if message:
logger_func[level]('ffpyplayer: {}'.format(message))
class SoundFFPy(Sound):
@staticmethod
def extensions():
return formats_in
def __init__(self, **kwargs):
self._ffplayer = None
self.quitted = False
self._log_callback_set = False
self._state = ''
self.state = 'stop'
if not get_log_callback():
set_log_callback(_log_callback)
self._log_callback_set = True
super(SoundFFPy, self).__init__(**kwargs)
def __del__(self):
self.unload()
if self._log_callback_set:
set_log_callback(None)
def _player_callback(self, selector, value):
if self._ffplayer is None:
return
if selector == 'quit':
def close(*args):
self.quitted = True
self.unload()
Clock.schedule_once(close, 0)
elif selector == 'eof':
Clock.schedule_once(self._do_eos, 0)
def load(self):
self.unload()
ff_opts = {'vn': True, 'sn': True} # only audio
self._ffplayer = MediaPlayer(self.source,
callback=self._player_callback,
loglevel='info', ff_opts=ff_opts)
player = self._ffplayer
player.set_volume(self.volume)
player.toggle_pause()
self._state = 'paused'
# wait until loaded or failed, shouldn't take long, but just to make
# sure metadata is available.
s = time.perf_counter()
while (player.get_metadata()['duration'] is None and
not self.quitted and time.perf_counter() - s < 10.):
time.sleep(0.005)
def unload(self):
if self._ffplayer:
self._ffplayer = None
self._state = ''
self.state = 'stop'
self.quitted = False
def play(self):
if self._state == 'playing':
super(SoundFFPy, self).play()
return
if not self._ffplayer:
self.load()
self._ffplayer.toggle_pause()
self._state = 'playing'
self.state = 'play'
super(SoundFFPy, self).play()
self.seek(0)
def stop(self):
if self._ffplayer and self._state == 'playing':
self._ffplayer.toggle_pause()
self._state = 'paused'
self.state = 'stop'
super(SoundFFPy, self).stop()
def seek(self, position):
if self._ffplayer is None:
return
self._ffplayer.seek(position, relative=False)
def get_pos(self):
if self._ffplayer is not None:
return self._ffplayer.get_pts()
return 0
def on_volume(self, instance, volume):
if self._ffplayer is not None:
self._ffplayer.set_volume(volume)
def _get_length(self):
if self._ffplayer is None:
return super(SoundFFPy, self)._get_length()
return self._ffplayer.get_metadata()['duration']
def _do_eos(self, *args):
if not self.loop:
self.stop()
else:
self.seek(0.)
SoundLoader.register(SoundFFPy)
@@ -0,0 +1,101 @@
'''
Audio Gstplayer
===============
.. versionadded:: 1.8.0
Implementation of a VideoBase with Kivy :class:`~kivy.lib.gstplayer.GstPlayer`
This player is the preferred player, using Gstreamer 1.0, working on both
Python 2 and 3.
'''
from kivy.lib.gstplayer import GstPlayer, get_gst_version
from kivy.core.audio import Sound, SoundLoader
from kivy.logger import Logger
from kivy.compat import PY2
from kivy.clock import Clock
from os.path import realpath
if PY2:
from urllib import pathname2url
else:
from urllib.request import pathname2url
Logger.info('AudioGstplayer: Using Gstreamer {}'.format(
'.'.join(map(str, get_gst_version()))))
def _on_gstplayer_message(mtype, message):
if mtype == 'error':
Logger.error('AudioGstplayer: {}'.format(message))
elif mtype == 'warning':
Logger.warning('AudioGstplayer: {}'.format(message))
elif mtype == 'info':
Logger.info('AudioGstplayer: {}'.format(message))
class SoundGstplayer(Sound):
@staticmethod
def extensions():
return ('wav', 'ogg', 'mp3', 'm4a', 'flac', 'mp4')
def __init__(self, **kwargs):
self.player = None
super(SoundGstplayer, self).__init__(**kwargs)
def _on_gst_eos_sync(self):
Clock.schedule_once(self._on_gst_eos, 0)
def _on_gst_eos(self, *dt):
if self.loop:
self.player.stop()
self.player.play()
else:
self.stop()
def load(self):
self.unload()
uri = self._get_uri()
self.player = GstPlayer(uri, None, self._on_gst_eos_sync,
_on_gstplayer_message)
self.player.load()
def play(self):
# we need to set the volume everytime, it seems that stopping + playing
# the sound reset the volume.
self.player.set_volume(self.volume)
self.player.play()
super(SoundGstplayer, self).play()
def stop(self):
self.player.stop()
super(SoundGstplayer, self).stop()
def unload(self):
if self.player:
self.player.unload()
self.player = None
def seek(self, position):
self.player.seek(position / self.length)
def get_pos(self):
return self.player.get_position()
def _get_length(self):
return self.player.get_duration()
def on_volume(self, instance, volume):
self.player.set_volume(volume)
def _get_uri(self):
uri = self.source
if not uri:
return
if '://' not in uri:
uri = 'file:' + pathname2url(realpath(uri))
return uri
SoundLoader.register(SoundGstplayer)
@@ -0,0 +1,127 @@
'''
AudioPygame: implementation of Sound with Pygame
.. warning::
Pygame has been deprecated and will be removed in the release after Kivy
1.11.0.
'''
__all__ = ('SoundPygame', )
from kivy.clock import Clock
from kivy.utils import platform, deprecated
from kivy.core.audio import Sound, SoundLoader
_platform = platform
try:
if _platform == 'android':
try:
import android.mixer as mixer
except ImportError:
# old python-for-android version
import android_mixer as mixer
else:
from pygame import mixer
except:
raise
# init pygame sound
mixer.pre_init(44100, -16, 2, 1024)
mixer.init()
mixer.set_num_channels(32)
class SoundPygame(Sound):
# XXX we don't set __slots__ here, to automatically add
# a dictionary. We need that to be able to use weakref for
# SoundPygame object. Otherwise, it failed with:
# TypeError: cannot create weak reference to 'SoundPygame' object
# We use our clock in play() method.
# __slots__ = ('_data', '_channel')
_check_play_ev = None
@staticmethod
def extensions():
if _platform == 'android':
return ('wav', 'ogg', 'mp3', 'm4a')
return ('wav', 'ogg')
@deprecated(
msg='Pygame has been deprecated and will be removed after 1.11.0')
def __init__(self, **kwargs):
self._data = None
self._channel = None
super(SoundPygame, self).__init__(**kwargs)
def _check_play(self, dt):
if self._channel is None:
return False
if self._channel.get_busy():
return
if self.loop:
def do_loop(dt):
self.play()
Clock.schedule_once(do_loop)
else:
self.stop()
return False
def play(self):
if not self._data:
return
self._data.set_volume(self.volume)
self._channel = self._data.play()
self.start_time = Clock.time()
# schedule event to check if the sound is still playing or not
self._check_play_ev = Clock.schedule_interval(self._check_play, 0.1)
super(SoundPygame, self).play()
def stop(self):
if not self._data:
return
self._data.stop()
# ensure we don't have anymore the callback
if self._check_play_ev is not None:
self._check_play_ev.cancel()
self._check_play_ev = None
self._channel = None
super(SoundPygame, self).stop()
def load(self):
self.unload()
if self.source is None:
return
self._data = mixer.Sound(self.source)
def unload(self):
self.stop()
self._data = None
def seek(self, position):
if not self._data:
return
if _platform == 'android' and self._channel:
self._channel.seek(position)
def get_pos(self):
if self._data is not None and self._channel:
if _platform == 'android':
return self._channel.get_pos()
return Clock.time() - self.start_time
return 0
def on_volume(self, instance, volume):
if self._data is not None:
self._data.set_volume(volume)
def _get_length(self):
if _platform == 'android' and self._channel:
return self._channel.get_length()
if self._data is not None:
return self._data.get_length()
return super(SoundPygame, self)._get_length()
SoundLoader.register(SoundPygame)
@@ -0,0 +1,151 @@
'''
Camera
======
Core class for acquiring the camera and converting its input into a
:class:`~kivy.graphics.texture.Texture`.
.. versionchanged:: 1.10.0
The pygst and videocapture providers have been removed.
.. versionchanged:: 1.8.0
There is now 2 distinct Gstreamer implementation: one using Gi/Gst
working for both Python 2+3 with Gstreamer 1.0, and one using PyGST
working only for Python 2 + Gstreamer 0.10.
'''
__all__ = ('CameraBase', 'Camera')
from kivy.utils import platform
from kivy.event import EventDispatcher
from kivy.logger import Logger
from kivy.core import core_select_lib
class CameraBase(EventDispatcher):
'''Abstract Camera Widget class.
Concrete camera classes must implement initialization and
frame capturing to a buffer that can be uploaded to the gpu.
:Parameters:
`index`: int
Source index of the camera.
`size`: tuple (int, int)
Size at which the image is drawn. If no size is specified,
it defaults to the resolution of the camera image.
`resolution`: tuple (int, int)
Resolution to try to request from the camera.
Used in the gstreamer pipeline by forcing the appsink caps
to this resolution. If the camera doesn't support the resolution,
a negotiation error might be thrown.
:Events:
`on_load`
Fired when the camera is loaded and the texture has become
available.
`on_texture`
Fired each time the camera texture is updated.
'''
__events__ = ('on_load', 'on_texture')
def __init__(self, **kwargs):
kwargs.setdefault('stopped', False)
kwargs.setdefault('resolution', (640, 480))
kwargs.setdefault('index', 0)
self.stopped = kwargs.get('stopped')
self._resolution = kwargs.get('resolution')
self._index = kwargs.get('index')
self._buffer = None
self._format = 'rgb'
self._texture = None
self.capture_device = None
kwargs.setdefault('size', self._resolution)
super(CameraBase, self).__init__()
self.init_camera()
if not self.stopped:
self.start()
def _set_resolution(self, res):
self._resolution = res
self.init_camera()
def _get_resolution(self):
return self._resolution
resolution = property(lambda self: self._get_resolution(),
lambda self, x: self._set_resolution(x),
doc='Resolution of camera capture (width, height)')
def _set_index(self, x):
if x == self._index:
return
self._index = x
self.init_camera()
def _get_index(self):
return self._x
index = property(lambda self: self._get_index(),
lambda self, x: self._set_index(x),
doc='Source index of the camera')
def _get_texture(self):
return self._texture
texture = property(lambda self: self._get_texture(),
doc='Return the camera texture with the latest capture')
def init_camera(self):
'''Initialize the camera (internal)'''
pass
def start(self):
'''Start the camera acquire'''
self.stopped = False
def stop(self):
'''Release the camera'''
self.stopped = True
def _update(self, dt):
'''Update the camera (internal)'''
pass
def _copy_to_gpu(self):
'''Copy the buffer into the texture.'''
if self._texture is None:
Logger.debug('Camera: copy_to_gpu() failed, _texture is None !')
return
self._texture.blit_buffer(self._buffer, colorfmt=self._format)
self._buffer = None
self.dispatch('on_texture')
def on_texture(self):
pass
def on_load(self):
pass
# Load the appropriate providers
providers = ()
if platform in ['macosx', 'ios']:
providers += (('avfoundation', 'camera_avfoundation',
'CameraAVFoundation'), )
elif platform == 'android':
providers += (('android', 'camera_android', 'CameraAndroid'), )
else:
providers += (('picamera', 'camera_picamera', 'CameraPiCamera'), )
providers += (('gi', 'camera_gi', 'CameraGi'), )
providers += (('opencv', 'camera_opencv', 'CameraOpenCV'), )
Camera = core_select_lib('camera', (providers))
@@ -0,0 +1,206 @@
from jnius import autoclass, PythonJavaClass, java_method
from kivy.clock import Clock
from kivy.graphics.texture import Texture
from kivy.graphics import Fbo, Callback, Rectangle
from kivy.core.camera import CameraBase
import threading
Camera = autoclass('android.hardware.Camera')
SurfaceTexture = autoclass('android.graphics.SurfaceTexture')
GL_TEXTURE_EXTERNAL_OES = autoclass(
'android.opengl.GLES11Ext').GL_TEXTURE_EXTERNAL_OES
ImageFormat = autoclass('android.graphics.ImageFormat')
class PreviewCallback(PythonJavaClass):
"""
Interface used to get back the preview frame of the Android Camera
"""
__javainterfaces__ = ('android.hardware.Camera$PreviewCallback', )
def __init__(self, callback):
super(PreviewCallback, self).__init__()
self._callback = callback
@java_method('([BLandroid/hardware/Camera;)V')
def onPreviewFrame(self, data, camera):
self._callback(data, camera)
class CameraAndroid(CameraBase):
"""
Implementation of CameraBase using Android API
"""
_update_ev = None
def __init__(self, **kwargs):
self._android_camera = None
self._preview_cb = PreviewCallback(self._on_preview_frame)
self._buflock = threading.Lock()
super(CameraAndroid, self).__init__(**kwargs)
def __del__(self):
self._release_camera()
def init_camera(self):
self._release_camera()
self._android_camera = Camera.open(self._index)
params = self._android_camera.getParameters()
width, height = self._resolution
params.setPreviewSize(width, height)
supported_focus_modes = self._android_camera.getParameters() \
.getSupportedFocusModes()
if supported_focus_modes.contains('continuous-picture'):
params.setFocusMode('continuous-picture')
self._android_camera.setParameters(params)
# self._android_camera.setDisplayOrientation()
self.fps = 30.
pf = params.getPreviewFormat()
assert pf == ImageFormat.NV21 # default format is NV21
self._bufsize = int(ImageFormat.getBitsPerPixel(pf) / 8. *
width * height)
self._camera_texture = Texture(width=width, height=height,
target=GL_TEXTURE_EXTERNAL_OES,
colorfmt='rgba')
self._surface_texture = SurfaceTexture(int(self._camera_texture.id))
self._android_camera.setPreviewTexture(self._surface_texture)
self._fbo = Fbo(size=self._resolution)
self._fbo['resolution'] = (float(width), float(height))
self._fbo.shader.fs = '''
#extension GL_OES_EGL_image_external : require
#ifdef GL_ES
precision highp float;
#endif
/* Outputs from the vertex shader */
varying vec4 frag_color;
varying vec2 tex_coord0;
/* uniform texture samplers */
uniform sampler2D texture0;
uniform samplerExternalOES texture1;
uniform vec2 resolution;
void main()
{
vec2 coord = vec2(tex_coord0.y * (
resolution.y / resolution.x), 1. -tex_coord0.x);
gl_FragColor = texture2D(texture1, tex_coord0);
}
'''
with self._fbo:
self._texture_cb = Callback(lambda instr:
self._camera_texture.bind)
Rectangle(size=self._resolution)
def _release_camera(self):
if self._android_camera is None:
return
self.stop()
self._android_camera.release()
self._android_camera = None
# clear texture and it'll be reset in `_update` pointing to new FBO
self._texture = None
del self._fbo, self._surface_texture, self._camera_texture
def _on_preview_frame(self, data, camera):
with self._buflock:
if self._buffer is not None:
# add buffer back for reuse
self._android_camera.addCallbackBuffer(self._buffer)
self._buffer = data
# check if frame grabbing works
# print self._buffer, len(self.frame_data)
def _refresh_fbo(self):
self._texture_cb.ask_update()
self._fbo.draw()
def start(self):
super(CameraAndroid, self).start()
with self._buflock:
self._buffer = None
for k in range(2): # double buffer
buf = b'\x00' * self._bufsize
self._android_camera.addCallbackBuffer(buf)
self._android_camera.setPreviewCallbackWithBuffer(self._preview_cb)
self._android_camera.startPreview()
if self._update_ev is not None:
self._update_ev.cancel()
self._update_ev = Clock.schedule_interval(self._update, 1 / self.fps)
def stop(self):
super(CameraAndroid, self).stop()
if self._update_ev is not None:
self._update_ev.cancel()
self._update_ev = None
self._android_camera.stopPreview()
self._android_camera.setPreviewCallbackWithBuffer(None)
# buffer queue cleared as well, to be recreated on next start
with self._buflock:
self._buffer = None
def _update(self, dt):
self._surface_texture.updateTexImage()
self._refresh_fbo()
if self._texture is None:
self._texture = self._fbo.texture
self.dispatch('on_load')
self._copy_to_gpu()
def _copy_to_gpu(self):
"""
A dummy placeholder (the image is already in GPU) to be consistent
with other providers.
"""
self.dispatch('on_texture')
def grab_frame(self):
"""
Grab current frame (thread-safe, minimal overhead)
"""
with self._buflock:
if self._buffer is None:
return None
buf = self._buffer.tostring()
return buf
def decode_frame(self, buf):
"""
Decode image data from grabbed frame.
This method depends on OpenCV and NumPy - however it is only used for
fetching the current frame as a NumPy array, and not required when
this :class:`CameraAndroid` provider is simply used by a
:class:`~kivy.uix.camera.Camera` widget.
"""
import numpy as np
from cv2 import cvtColor
w, h = self._resolution
arr = np.fromstring(buf, 'uint8').reshape((h + h // 2, w))
arr = cvtColor(arr, 93) # NV21 -> BGR
return arr
def read_frame(self):
"""
Grab and decode frame in one call
"""
return self.decode_frame(self.grab_frame())
@staticmethod
def get_camera_count():
"""
Get the number of available cameras.
"""
return Camera.getNumberOfCameras()
@@ -0,0 +1,170 @@
'''
Gi Camera
=========
Implement CameraBase with Gi / Gstreamer, working on both Python 2 and 3
'''
__all__ = ('CameraGi', )
from gi.repository import Gst
from kivy.clock import Clock
from kivy.graphics.texture import Texture
from kivy.core.camera import CameraBase
from kivy.support import install_gobject_iteration
from kivy.logger import Logger
from ctypes import Structure, c_void_p, c_int, string_at
from weakref import ref
import atexit
# initialize the camera/gi. if the older version is used, don't use camera_gi.
Gst.init(None)
version = Gst.version()
if version < (1, 0, 0, 0):
raise Exception('Cannot use camera_gi, Gstreamer < 1.0 is not supported.')
Logger.info('CameraGi: Using Gstreamer {}'.format(
'.'.join(['{}'.format(x) for x in Gst.version()])))
install_gobject_iteration()
class _MapInfo(Structure):
_fields_ = [
('memory', c_void_p),
('flags', c_int),
('data', c_void_p)]
# we don't care about the rest
def _on_cameragi_unref(obj):
if obj in CameraGi._instances:
CameraGi._instances.remove(obj)
class CameraGi(CameraBase):
'''Implementation of CameraBase using GStreamer
:Parameters:
`video_src`: str, default is 'v4l2src'
Other tested options are: 'dc1394src' for firewire
dc camera (e.g. firefly MV). Any gstreamer video source
should potentially work.
Theoretically a longer string using "!" can be used
describing the first part of a gstreamer pipeline.
'''
_instances = []
def __init__(self, **kwargs):
self._pipeline = None
self._camerasink = None
self._decodebin = None
self._texturesize = None
self._video_src = kwargs.get('video_src', 'v4l2src')
wk = ref(self, _on_cameragi_unref)
CameraGi._instances.append(wk)
super(CameraGi, self).__init__(**kwargs)
def init_camera(self):
# TODO: This doesn't work when camera resolution is resized at runtime.
# There must be some other way to release the camera?
if self._pipeline:
self._pipeline = None
video_src = self._video_src
if video_src == 'v4l2src':
video_src += ' device=/dev/video%d' % self._index
elif video_src == 'dc1394src':
video_src += ' camera-number=%d' % self._index
if Gst.version() < (1, 0, 0, 0):
caps = ('video/x-raw-rgb,red_mask=(int)0xff0000,'
'green_mask=(int)0x00ff00,blue_mask=(int)0x0000ff')
pl = ('{} ! decodebin name=decoder ! ffmpegcolorspace ! '
'appsink name=camerasink emit-signals=True caps={}')
else:
caps = 'video/x-raw,format=RGB'
pl = '{} ! decodebin name=decoder ! videoconvert ! appsink ' + \
'name=camerasink emit-signals=True caps={}'
self._pipeline = Gst.parse_launch(pl.format(video_src, caps))
self._camerasink = self._pipeline.get_by_name('camerasink')
self._camerasink.connect('new-sample', self._gst_new_sample)
self._decodebin = self._pipeline.get_by_name('decoder')
if self._camerasink and not self.stopped:
self.start()
def _gst_new_sample(self, *largs):
sample = self._camerasink.emit('pull-sample')
if sample is None:
return False
self._sample = sample
if self._texturesize is None:
# try to get the camera image size
for pad in self._decodebin.srcpads:
s = pad.get_current_caps().get_structure(0)
self._texturesize = (
s.get_value('width'),
s.get_value('height'))
Clock.schedule_once(self._update)
return False
Clock.schedule_once(self._update)
return False
def start(self):
super(CameraGi, self).start()
self._pipeline.set_state(Gst.State.PLAYING)
def stop(self):
super(CameraGi, self).stop()
self._pipeline.set_state(Gst.State.PAUSED)
def unload(self):
self._pipeline.set_state(Gst.State.NULL)
def _update(self, dt):
sample, self._sample = self._sample, None
if sample is None:
return
if self._texture is None and self._texturesize is not None:
self._texture = Texture.create(
size=self._texturesize, colorfmt='rgb')
self._texture.flip_vertical()
self.dispatch('on_load')
# decode sample
# read the data from the buffer memory
try:
buf = sample.get_buffer()
result, mapinfo = buf.map(Gst.MapFlags.READ)
# We cannot get the data out of mapinfo, using Gst 1.0.6 + Gi 3.8.0
# related bug report:
# https://bugzilla.gnome.org/show_bug.cgi?id=6t8663
# ie: mapinfo.data is normally a char*, but here, we have an int
# So right now, we use ctypes instead to read the mapinfo ourself.
addr = mapinfo.__hash__()
c_mapinfo = _MapInfo.from_address(addr)
# now get the memory
self._buffer = string_at(c_mapinfo.data, mapinfo.size)
self._copy_to_gpu()
finally:
if mapinfo is not None:
buf.unmap(mapinfo)
@atexit.register
def camera_gi_clean():
# if we leave the python process with some video running, we can hit a
# segfault. This is forcing the stop/unload of all remaining videos before
# exiting the python process.
for weakcamera in CameraGi._instances:
camera = weakcamera()
if isinstance(camera, CameraGi):
camera.stop()
camera.unload()
@@ -0,0 +1,163 @@
'''
OpenCV Camera: Implement CameraBase with OpenCV
'''
#
# TODO: make usage of thread or multiprocess
#
from __future__ import division
__all__ = ('CameraOpenCV')
from kivy.logger import Logger
from kivy.clock import Clock
from kivy.graphics.texture import Texture
from kivy.core.camera import CameraBase
try:
# opencv 1 case
import opencv as cv
try:
import opencv.highgui as hg
except ImportError:
class Hg(object):
'''
On OSX, not only are the import names different,
but the API also differs.
There is no module called 'highgui' but the names are
directly available in the 'cv' module.
Some of them even have a different names.
Therefore we use this proxy object.
'''
def __getattr__(self, attr):
if attr.startswith('cv'):
attr = attr[2:]
got = getattr(cv, attr)
return got
hg = Hg()
except ImportError:
# opencv 2 case (and also opencv 3, because it still uses cv2 module name)
try:
import cv2
# here missing this OSX specific highgui thing.
# I'm not on OSX so don't know if it is still valid in opencv >= 2
except ImportError:
raise
class CameraOpenCV(CameraBase):
'''
Implementation of CameraBase using OpenCV
'''
_update_ev = None
def __init__(self, **kwargs):
# we will need it, because constants have
# different access paths between ver. 2 and 3
try:
self.opencvMajorVersion = int(cv.__version__[0])
except NameError:
self.opencvMajorVersion = int(cv2.__version__[0])
self._device = None
super(CameraOpenCV, self).__init__(**kwargs)
def init_camera(self):
# consts have changed locations between versions 2 and 3
if self.opencvMajorVersion in (3, 4):
PROPERTY_WIDTH = cv2.CAP_PROP_FRAME_WIDTH
PROPERTY_HEIGHT = cv2.CAP_PROP_FRAME_HEIGHT
PROPERTY_FPS = cv2.CAP_PROP_FPS
elif self.opencvMajorVersion == 2:
PROPERTY_WIDTH = cv2.cv.CV_CAP_PROP_FRAME_WIDTH
PROPERTY_HEIGHT = cv2.cv.CV_CAP_PROP_FRAME_HEIGHT
PROPERTY_FPS = cv2.cv.CV_CAP_PROP_FPS
elif self.opencvMajorVersion == 1:
PROPERTY_WIDTH = cv.CV_CAP_PROP_FRAME_WIDTH
PROPERTY_HEIGHT = cv.CV_CAP_PROP_FRAME_HEIGHT
PROPERTY_FPS = cv.CV_CAP_PROP_FPS
Logger.debug('Using opencv ver.' + str(self.opencvMajorVersion))
if self.opencvMajorVersion == 1:
# create the device
self._device = hg.cvCreateCameraCapture(self._index)
# Set preferred resolution
cv.SetCaptureProperty(self._device, cv.CV_CAP_PROP_FRAME_WIDTH,
self.resolution[0])
cv.SetCaptureProperty(self._device, cv.CV_CAP_PROP_FRAME_HEIGHT,
self.resolution[1])
# and get frame to check if it's ok
frame = hg.cvQueryFrame(self._device)
# Just set the resolution to the frame we just got, but don't use
# self.resolution for that as that would cause an infinite
# recursion with self.init_camera (but slowly as we'd have to
# always get a frame).
self._resolution = (int(frame.width), int(frame.height))
# get fps
self.fps = cv.GetCaptureProperty(self._device, cv.CV_CAP_PROP_FPS)
elif self.opencvMajorVersion in (2, 3, 4):
# create the device
self._device = cv2.VideoCapture(self._index)
# Set preferred resolution
self._device.set(PROPERTY_WIDTH,
self.resolution[0])
self._device.set(PROPERTY_HEIGHT,
self.resolution[1])
# and get frame to check if it's ok
ret, frame = self._device.read()
# source:
# http://stackoverflow.com/questions/32468371/video-capture-propid-parameters-in-opencv # noqa
self._resolution = (int(frame.shape[1]), int(frame.shape[0]))
# get fps
self.fps = self._device.get(PROPERTY_FPS)
if self.fps == 0 or self.fps == 1:
self.fps = 1.0 / 30
elif self.fps > 1:
self.fps = 1.0 / self.fps
if not self.stopped:
self.start()
def _update(self, dt):
if self.stopped:
return
if self._texture is None:
# Create the texture
self._texture = Texture.create(self._resolution)
self._texture.flip_vertical()
self.dispatch('on_load')
try:
ret, frame = self._device.read()
self._format = 'bgr'
try:
self._buffer = frame.imageData
except AttributeError:
# frame is already of type ndarray
# which can be reshaped to 1-d.
self._buffer = frame.reshape(-1)
self._copy_to_gpu()
except:
Logger.exception('OpenCV: Couldn\'t get image from Camera')
def start(self):
super(CameraOpenCV, self).start()
if self._update_ev is not None:
self._update_ev.cancel()
self._update_ev = Clock.schedule_interval(self._update, self.fps)
def stop(self):
super(CameraOpenCV, self).stop()
if self._update_ev is not None:
self._update_ev.cancel()
self._update_ev = None
@@ -0,0 +1,96 @@
'''
PiCamera Camera: Implement CameraBase with PiCamera
'''
#
# TODO: make usage of thread or multiprocess
#
__all__ = ('CameraPiCamera', )
from math import ceil
from kivy.logger import Logger
from kivy.clock import Clock
from kivy.graphics.texture import Texture
from kivy.core.camera import CameraBase
from picamera import PiCamera
import numpy
class CameraPiCamera(CameraBase):
'''Implementation of CameraBase using PiCamera
'''
_update_ev = None
def __init__(self, **kwargs):
self._camera = None
self._format = 'bgr'
self._framerate = kwargs.get('framerate', 30)
super(CameraPiCamera, self).__init__(**kwargs)
def init_camera(self):
if self._camera is not None:
self._camera.close()
self._camera = PiCamera()
self._camera.resolution = self.resolution
self._camera.framerate = self._framerate
self._camera.iso = 800
self.fps = 1. / self._framerate
if not self.stopped:
self.start()
def raw_buffer_size(self):
'''Round buffer size up to 32x16 blocks.
See https://picamera.readthedocs.io/en/release-1.13/recipes2.html#capturing-to-a-numpy-array
''' # noqa
return (
ceil(self.resolution[0] / 32.) * 32,
ceil(self.resolution[1] / 16.) * 16
)
def _update(self, dt):
if self.stopped:
return
if self._texture is None:
# Create the texture
self._texture = Texture.create(self._resolution)
self._texture.flip_vertical()
self.dispatch('on_load')
try:
bufsize = self.raw_buffer_size()
output = numpy.empty(
(bufsize[0] * bufsize[1] * 3,), dtype=numpy.uint8)
self._camera.capture(output, self._format, use_video_port=True)
# Trim the buffer to fit the actual requested resolution.
# TODO: Is there a simpler way to do all this reshuffling?
output = output.reshape((bufsize[0], bufsize[1], 3))
output = output[:self.resolution[0], :self.resolution[1], :]
self._buffer = output.reshape(
(self.resolution[0] * self.resolution[1] * 3,))
self._copy_to_gpu()
except KeyboardInterrupt:
raise
except Exception:
Logger.exception('PiCamera: Couldn\'t get image from Camera')
def start(self):
super(CameraPiCamera, self).start()
if self._update_ev is not None:
self._update_ev.cancel()
self._update_ev = Clock.schedule_interval(self._update, self.fps)
def stop(self):
super(CameraPiCamera, self).stop()
if self._update_ev is not None:
self._update_ev.cancel()
self._update_ev = None
@@ -0,0 +1,157 @@
'''
Clipboard
=========
Core class for accessing the Clipboard. If we are not able to access the
system clipboard, a fake one will be used.
Usage example:
.. code-block:: kv
#:import Clipboard kivy.core.clipboard.Clipboard
Button:
on_release:
self.text = Clipboard.paste()
Clipboard.copy('Data')
'''
__all__ = ('ClipboardBase', 'Clipboard')
from kivy import Logger
from kivy.core import core_select_lib
from kivy.utils import platform
from kivy.setupconfig import USE_SDL2
class ClipboardBase(object):
def get(self, mimetype):
'''Get the current data in clipboard, using the mimetype if possible.
You not use this method directly. Use :meth:`paste` instead.
'''
pass
def put(self, data, mimetype):
'''Put data on the clipboard, and attach a mimetype.
You should not use this method directly. Use :meth:`copy` instead.
'''
pass
def get_types(self):
'''Return a list of supported mimetypes
'''
return []
def _ensure_clipboard(self):
''' Ensure that the clipboard has been properly initialized.
'''
if hasattr(self, '_clip_mime_type'):
return
if platform == 'win':
self._clip_mime_type = 'text/plain;charset=utf-8'
# windows clipboard uses a utf-16 little endian encoding
self._encoding = 'utf-16-le'
elif platform == 'linux':
self._clip_mime_type = 'text/plain;charset=utf-8'
self._encoding = 'utf-8'
else:
self._clip_mime_type = 'text/plain'
self._encoding = 'utf-8'
def copy(self, data=''):
''' Copy the value provided in argument `data` into current clipboard.
If data is not of type string it will be converted to string.
.. versionadded:: 1.9.0
'''
if data:
self._copy(data)
def paste(self):
''' Get text from the system clipboard and return it a usable string.
.. versionadded:: 1.9.0
'''
return self._paste()
def _copy(self, data):
self._ensure_clipboard()
if not isinstance(data, bytes):
data = data.encode(self._encoding)
self.put(data, self._clip_mime_type)
def _paste(self):
self._ensure_clipboard()
_clip_types = Clipboard.get_types()
mime_type = self._clip_mime_type
if mime_type not in _clip_types:
mime_type = 'text/plain'
data = self.get(mime_type)
if data is not None:
# decode only if we don't have unicode
# we would still need to decode from utf-16 (windows)
# data is of type bytes in PY3
if isinstance(data, bytes):
data = data.decode(self._encoding, 'ignore')
# remove null strings mostly a windows issue
data = data.replace(u'\x00', u'')
return data
return u''
# load clipboard implementation
_clipboards = []
if platform == 'android':
_clipboards.append(
('android', 'clipboard_android', 'ClipboardAndroid'))
elif platform == 'macosx':
_clipboards.append(
('nspaste', 'clipboard_nspaste', 'ClipboardNSPaste'))
elif platform == 'win':
_clipboards.append(
('winctypes', 'clipboard_winctypes', 'ClipboardWindows'))
elif platform == 'linux':
_clipboards.append(
('xclip', 'clipboard_xclip', 'ClipboardXclip'))
_clipboards.append(
('xsel', 'clipboard_xsel', 'ClipboardXsel'))
_clipboards.append(
('dbusklipper', 'clipboard_dbusklipper', 'ClipboardDbusKlipper'))
_clipboards.append(
('gtk3', 'clipboard_gtk3', 'ClipboardGtk3'))
if USE_SDL2:
_clipboards.append(
('sdl2', 'clipboard_sdl2', 'ClipboardSDL2'))
else:
_clipboards.append(
('pygame', 'clipboard_pygame', 'ClipboardPygame'))
_clipboards.append(
('dummy', 'clipboard_dummy', 'ClipboardDummy'))
Clipboard = core_select_lib('clipboard', _clipboards, True)
CutBuffer = None
if platform == 'linux':
_cutbuffers = [
('xclip', 'clipboard_xclip', 'ClipboardXclip'),
('xsel', 'clipboard_xsel', 'ClipboardXsel'),
]
if Clipboard.__class__.__name__ in (c[2] for c in _cutbuffers):
CutBuffer = Clipboard
else:
CutBuffer = core_select_lib('cutbuffer', _cutbuffers, True,
basemodule='clipboard')
if CutBuffer:
Logger.info('CutBuffer: cut buffer support enabled')
@@ -0,0 +1,36 @@
'''
Clipboard ext: base class for external command clipboards
'''
__all__ = ('ClipboardExternalBase', )
from kivy.core.clipboard import ClipboardBase
class ClipboardExternalBase(ClipboardBase):
@staticmethod
def _clip(inout, selection):
raise NotImplementedError('clip method not implemented')
def get(self, mimetype='text/plain'):
p = self._clip('out', 'clipboard')
data, _ = p.communicate()
return data
def put(self, data, mimetype='text/plain'):
p = self._clip('in', 'clipboard')
p.communicate(data)
def get_cutbuffer(self):
p = self._clip('out', 'primary')
data, _ = p.communicate()
return data.decode('utf8')
def set_cutbuffer(self, data):
if not isinstance(data, bytes):
data = data.encode('utf8')
p = self._clip('in', 'primary')
p.communicate(data)
def get_types(self):
return [u'text/plain']
@@ -0,0 +1,91 @@
'''
Clipboard Android
=================
Android implementation of Clipboard provider, using Pyjnius.
'''
__all__ = ('ClipboardAndroid', )
from kivy import Logger
from kivy.core.clipboard import ClipboardBase
from jnius import autoclass, cast
from android.runnable import run_on_ui_thread
from android import python_act
AndroidString = autoclass('java.lang.String')
PythonActivity = python_act
Context = autoclass('android.content.Context')
VER = autoclass('android.os.Build$VERSION')
sdk = VER.SDK_INT
class ClipboardAndroid(ClipboardBase):
def __init__(self):
super(ClipboardAndroid, self).__init__()
self._clipboard = None
self._data = dict()
self._data['text/plain'] = None
self._data['application/data'] = None
PythonActivity._clipboard = None
def get(self, mimetype='text/plain'):
return self._get(mimetype).encode('utf-8')
def put(self, data, mimetype='text/plain'):
self._set(data, mimetype)
def get_types(self):
return list(self._data.keys())
@run_on_ui_thread
def _initialize_clipboard(self):
PythonActivity._clipboard = cast(
'android.app.Activity',
PythonActivity.mActivity).getSystemService(
Context.CLIPBOARD_SERVICE)
def _get_clipboard(f):
def called(*args, **kargs):
self = args[0]
if not PythonActivity._clipboard:
self._initialize_clipboard()
import time
while not PythonActivity._clipboard:
time.sleep(.01)
return f(*args, **kargs)
return called
@_get_clipboard
def _get(self, mimetype='text/plain'):
clippy = PythonActivity._clipboard
data = ''
if sdk < 11:
data = clippy.getText()
else:
ClipDescription = autoclass('android.content.ClipDescription')
primary_clip = clippy.getPrimaryClip()
if primary_clip:
try:
data = primary_clip.getItemAt(0)
if data:
data = data.coerceToText(
PythonActivity.mActivity.getApplicationContext())
except Exception:
Logger.exception('Clipboard: failed to paste')
return data
@_get_clipboard
def _set(self, data, mimetype):
clippy = PythonActivity._clipboard
if sdk < 11:
# versions previous to honeycomb
clippy.setText(AndroidString(data))
else:
ClipData = autoclass('android.content.ClipData')
new_clip = ClipData.newPlainText(AndroidString(""),
AndroidString(data))
# put text data onto clipboard
clippy.setPrimaryClip(new_clip)
@@ -0,0 +1,41 @@
'''
Clipboard Dbus: an implementation of the Clipboard using dbus and klipper.
'''
__all__ = ('ClipboardDbusKlipper', )
from kivy.utils import platform
from kivy.core.clipboard import ClipboardBase
if platform != 'linux':
raise SystemError('unsupported platform for dbus kde clipboard')
try:
import dbus
bus = dbus.SessionBus()
proxy = bus.get_object("org.kde.klipper", "/klipper")
except:
raise
class ClipboardDbusKlipper(ClipboardBase):
_is_init = False
def init(self):
if ClipboardDbusKlipper._is_init:
return
self.iface = dbus.Interface(proxy, "org.kde.klipper.klipper")
ClipboardDbusKlipper._is_init = True
def get(self, mimetype='text/plain'):
self.init()
return str(self.iface.getClipboardContents())
def put(self, data, mimetype='text/plain'):
self.init()
self.iface.setClipboardContents(data.replace('\x00', ''))
def get_types(self):
self.init()
return [u'text/plain']
@@ -0,0 +1,26 @@
'''
Clipboard Dummy: an internal implementation that does not use the system
clipboard.
'''
__all__ = ('ClipboardDummy', )
from kivy.core.clipboard import ClipboardBase
class ClipboardDummy(ClipboardBase):
def __init__(self):
super(ClipboardDummy, self).__init__()
self._data = dict()
self._data['text/plain'] = None
self._data['application/data'] = None
def get(self, mimetype='text/plain'):
return self._data.get(mimetype, None)
def put(self, data, mimetype='text/plain'):
self._data[mimetype] = data
def get_types(self):
return list(self._data.keys())
@@ -0,0 +1,47 @@
'''
Clipboard Gtk3: an implementation of the Clipboard using Gtk3.
'''
__all__ = ('ClipboardGtk3',)
from kivy.utils import platform
from kivy.support import install_gobject_iteration
from kivy.core.clipboard import ClipboardBase
if platform != 'linux':
raise SystemError('unsupported platform for gtk3 clipboard')
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
class ClipboardGtk3(ClipboardBase):
_is_init = False
def init(self):
if self._is_init:
return
install_gobject_iteration()
self._is_init = True
def get(self, mimetype='text/plain;charset=utf-8'):
self.init()
if mimetype == 'text/plain;charset=utf-8':
contents = clipboard.wait_for_text()
if contents:
return contents
return ''
def put(self, data, mimetype='text/plain;charset=utf-8'):
self.init()
if mimetype == 'text/plain;charset=utf-8':
text = data.decode(self._encoding)
clipboard.set_text(text, -1)
clipboard.store()
def get_types(self):
self.init()
return ['text/plain;charset=utf-8']
@@ -0,0 +1,44 @@
'''
Clipboard OsX: implementation of clipboard using Appkit
'''
__all__ = ('ClipboardNSPaste', )
from kivy.core.clipboard import ClipboardBase
from kivy.utils import platform
if platform != 'macosx':
raise SystemError('Unsupported platform for appkit clipboard.')
try:
from pyobjus import autoclass
from pyobjus.dylib_manager import load_framework, INCLUDE
load_framework(INCLUDE.AppKit)
except ImportError:
raise SystemError('Pyobjus not installed. Please run the following'
' command to install it. `pip install --user pyobjus`')
NSPasteboard = autoclass('NSPasteboard')
NSString = autoclass('NSString')
class ClipboardNSPaste(ClipboardBase):
def __init__(self):
super(ClipboardNSPaste, self).__init__()
self._clipboard = NSPasteboard.generalPasteboard()
def get(self, mimetype='text/plain'):
pb = self._clipboard
data = pb.stringForType_('public.utf8-plain-text')
if not data:
return ""
return data.UTF8String()
def put(self, data, mimetype='text/plain'):
pb = self._clipboard
pb.clearContents()
utf8 = NSString.alloc().initWithUTF8String_(data)
pb.setString_forType_(utf8, 'public.utf8-plain-text')
def get_types(self):
return list('text/plain',)
@@ -0,0 +1,67 @@
'''
Clipboard Pygame: an implementation of the Clipboard using pygame.scrap.
.. warning::
Pygame has been deprecated and will be removed in the release after Kivy
1.11.0.
'''
__all__ = ('ClipboardPygame', )
from kivy.utils import platform
from kivy.core.clipboard import ClipboardBase
from kivy.utils import deprecated
if platform not in ('win', 'linux', 'macosx'):
raise SystemError('unsupported platform for pygame clipboard')
try:
import pygame
import pygame.scrap
except:
raise
class ClipboardPygame(ClipboardBase):
_is_init = False
_types = None
_aliases = {
'text/plain;charset=utf-8': 'UTF8_STRING'
}
@deprecated(
msg='Pygame has been deprecated and will be removed after 1.11.0')
def __init__(self, *largs, **kwargs):
super(ClipboardPygame, self).__init__(*largs, **kwargs)
def init(self):
if ClipboardPygame._is_init:
return
pygame.scrap.init()
ClipboardPygame._is_init = True
def get(self, mimetype='text/plain'):
self.init()
mimetype = self._aliases.get(mimetype, mimetype)
text = pygame.scrap.get(mimetype)
return text
def put(self, data, mimetype='text/plain'):
self.init()
mimetype = self._aliases.get(mimetype, mimetype)
pygame.scrap.put(mimetype, data)
def get_types(self):
if not self._types:
self.init()
types = pygame.scrap.get_types()
for mime, pygtype in list(self._aliases.items())[:]:
if mime in types:
del self._aliases[mime]
if pygtype in types:
types.append(mime)
self._types = types
return self._types
@@ -0,0 +1,36 @@
'''
Clipboard SDL2: an implementation of the Clipboard using sdl2.
'''
__all__ = ('ClipboardSDL2', )
from kivy.utils import platform
from kivy.core.clipboard import ClipboardBase
if platform not in ('win', 'linux', 'macosx', 'android', 'ios'):
raise SystemError('unsupported platform for sdl2 clipboard')
try:
from kivy.core.clipboard._clipboard_sdl2 import (
_get_text, _has_text, _set_text)
except ImportError:
from kivy.core import handle_win_lib_import_error
handle_win_lib_import_error(
'Clipboard', 'sdl2', 'kivy.core.clipboard._clipboard_sdl2')
raise
class ClipboardSDL2(ClipboardBase):
def get(self, mimetype):
return _get_text() if _has_text() else ''
def _ensure_clipboard(self):
super(ClipboardSDL2, self)._ensure_clipboard()
self._encoding = 'utf8'
def put(self, data=b'', mimetype='text/plain'):
_set_text(data)
def get_types(self):
return ['text/plain']
@@ -0,0 +1,107 @@
'''
Clipboard windows: an implementation of the Clipboard using ctypes.
'''
__all__ = ('ClipboardWindows', )
from kivy.utils import platform
from kivy.core.clipboard import ClipboardBase
if platform != 'win':
raise SystemError('unsupported platform for Windows clipboard')
import ctypes
from ctypes import wintypes
user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32
msvcrt = ctypes.cdll.msvcrt
c_char_p = ctypes.c_char_p
c_wchar_p = ctypes.c_wchar_p
GlobalLock = kernel32.GlobalLock
GlobalLock.argtypes = [wintypes.HGLOBAL]
GlobalLock.restype = wintypes.LPVOID
GlobalUnlock = kernel32.GlobalUnlock
GlobalUnlock.argtypes = [wintypes.HGLOBAL]
GlobalUnlock.restype = wintypes.BOOL
CF_UNICODETEXT = 13
GMEM_MOVEABLE = 0x0002
class ClipboardWindows(ClipboardBase):
def _copy(self, data):
self._ensure_clipboard()
self.put(data, self._clip_mime_type)
def get(self, mimetype='text/plain'):
GetClipboardData = user32.GetClipboardData
GetClipboardData.argtypes = [wintypes.UINT]
GetClipboardData.restype = wintypes.HANDLE
user32.OpenClipboard(user32.GetActiveWindow())
# GetClipboardData returns a HANDLE to the clipboard data
# which is a memory object containing the data
# See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclipboarddata # noqa: E501
pcontents = GetClipboardData(CF_UNICODETEXT)
# if someone pastes a FILE, the content is None for SCF 13
# and the clipboard is locked if not closed properly
if not pcontents:
user32.CloseClipboard()
return ''
# The handle returned by GetClipboardData is a memory object
# and needs to be locked to get the actual pointer to the data
pcontents_locked = GlobalLock(pcontents)
data = c_wchar_p(pcontents_locked).value
GlobalUnlock(pcontents)
user32.CloseClipboard()
return data
def put(self, text, mimetype="text/plain"):
SetClipboardData = user32.SetClipboardData
SetClipboardData.argtypes = [wintypes.UINT, wintypes.HANDLE]
SetClipboardData.restype = wintypes.HANDLE
GlobalAlloc = kernel32.GlobalAlloc
GlobalAlloc.argtypes = [wintypes.UINT, ctypes.c_size_t]
GlobalAlloc.restype = wintypes.HGLOBAL
user32.OpenClipboard(user32.GetActiveWindow())
user32.EmptyClipboard()
# The wsclen function returns the number of
# wide characters in a string (not including the null character)
text_len = msvcrt.wcslen(text) + 1
# According to the docs regarding SetClipboardDatam, if the hMem
# parameter identifies a memory object, the object must have
# been allocated using the GMEM_MOVEABLE flag.
# See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclipboarddata # noqa: E501
# The size of the memory object is the number of wide characters in
# the string plus one for the terminating null character
hCd = GlobalAlloc(
GMEM_MOVEABLE, ctypes.sizeof(ctypes.c_wchar) * text_len
)
# Since the memory object is allocated with GMEM_MOVEABLE, should be
# locked to get the actual pointer to the data.
hCd_locked = GlobalLock(hCd)
ctypes.memmove(
c_wchar_p(hCd_locked),
c_wchar_p(text),
ctypes.sizeof(ctypes.c_wchar) * text_len,
)
GlobalUnlock(hCd)
# Finally, set the clipboard data (and then close the clipboard)
SetClipboardData(CF_UNICODETEXT, hCd)
user32.CloseClipboard()
def get_types(self):
return ['text/plain']
@@ -0,0 +1,29 @@
'''
Clipboard xclip: an implementation of the Clipboard using xclip
command line tool.
'''
__all__ = ('ClipboardXclip', )
from kivy.utils import platform
from kivy.core.clipboard._clipboard_ext import ClipboardExternalBase
if platform != 'linux':
raise SystemError('unsupported platform for xclip clipboard')
try:
import subprocess
p = subprocess.Popen(['xclip', '-version'], stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL)
p.communicate()
except:
raise
class ClipboardXclip(ClipboardExternalBase):
@staticmethod
def _clip(inout, selection):
pipe = {'std' + inout: subprocess.PIPE}
return subprocess.Popen(
['xclip', '-' + inout, '-selection', selection], **pipe)
@@ -0,0 +1,26 @@
'''
Clipboard xsel: an implementation of the Clipboard using xsel command line
tool.
'''
__all__ = ('ClipboardXsel', )
from kivy.utils import platform
from kivy.core.clipboard._clipboard_ext import ClipboardExternalBase
if platform != 'linux':
raise SystemError('unsupported platform for xsel clipboard')
import subprocess
p = subprocess.Popen(['xsel', '--version'], stdout=subprocess.PIPE)
p.communicate(timeout=1)
class ClipboardXsel(ClipboardExternalBase):
@staticmethod
def _clip(inout, selection):
pipe = {'std' + inout: subprocess.PIPE}
sel = 'b' if selection == 'clipboard' else selection[0]
io = inout[0]
return subprocess.Popen(
['xsel', '-' + sel + io], **pipe)
@@ -0,0 +1,90 @@
# pylint: disable=W0611
'''
OpenGL
======
Select and use the best OpenGL library available. Depending on your system, the
core provider can select an OpenGL ES or a 'classic' desktop OpenGL library.
'''
import sys
from os import environ
MIN_REQUIRED_GL_VERSION = (2, 0)
def msgbox(message):
if sys.platform == 'win32':
import ctypes
from ctypes.wintypes import LPCWSTR
ctypes.windll.user32.MessageBoxW(None, LPCWSTR(message),
u"Kivy Fatal Error", 0)
sys.exit(1)
if 'KIVY_DOC' not in environ:
from kivy.logger import Logger
from kivy.graphics import gl_init_resources
from kivy.graphics.opengl_utils import gl_get_version
from kivy.graphics.opengl import (
GL_VERSION,
GL_VENDOR,
GL_RENDERER,
GL_MAX_TEXTURE_IMAGE_UNITS,
GL_MAX_TEXTURE_SIZE,
GL_SHADING_LANGUAGE_VERSION,
glGetString,
glGetIntegerv,
gl_init_symbols,
)
from kivy.graphics.cgl import cgl_get_initialized_backend_name
from kivy.utils import platform
def init_gl(allowed=[], ignored=[]):
gl_init_symbols(allowed, ignored)
print_gl_version()
gl_init_resources()
def print_gl_version():
backend = cgl_get_initialized_backend_name()
Logger.info('GL: Backend used <{}>'.format(backend))
version = glGetString(GL_VERSION)
vendor = glGetString(GL_VENDOR)
renderer = glGetString(GL_RENDERER)
Logger.info('GL: OpenGL version <{0}>'.format(version))
Logger.info('GL: OpenGL vendor <{0}>'.format(vendor))
Logger.info('GL: OpenGL renderer <{0}>'.format(renderer))
# Let the user know if his graphics hardware/drivers are too old
major, minor = gl_get_version()
Logger.info('GL: OpenGL parsed version: %d, %d' % (major, minor))
if ((major, minor) < MIN_REQUIRED_GL_VERSION and backend != "mock"):
if hasattr(sys, "_kivy_opengl_required_func"):
sys._kivy_opengl_required_func(major, minor, version, vendor,
renderer)
else:
msg = (
'GL: Minimum required OpenGL version (2.0) NOT found!\n\n'
'OpenGL version detected: {0}.{1}\n\n'
'Version: {2}\nVendor: {3}\nRenderer: {4}\n\n'
'Try upgrading your graphics drivers and/or your '
'graphics hardware in case of problems.\n\n'
'The application will leave now.').format(
major, minor, version, vendor, renderer)
Logger.critical(msg)
msgbox(msg)
if platform != 'android':
# XXX in the android emulator (latest version at 22 march 2013),
# this call was segfaulting the gl stack.
Logger.info('GL: Shading version <{0}>'.format(glGetString(
GL_SHADING_LANGUAGE_VERSION)))
Logger.info('GL: Texture max size <{0}>'.format(glGetIntegerv(
GL_MAX_TEXTURE_SIZE)[0]))
Logger.info('GL: Texture max units <{0}>'.format(glGetIntegerv(
GL_MAX_TEXTURE_IMAGE_UNITS)[0]))
# To be able to use our GL provider, we must have a window
# Automatically import window auto to ensure the default window creation
import kivy.core.window # NOQA
@@ -0,0 +1,996 @@
'''
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 SDL2, pygame, 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")
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()
'''
import re
from base64 import b64decode
from filetype import guess_extension
__all__ = ('Image', 'ImageLoader', 'ImageData')
from kivy.event import EventDispatcher
from kivy.core import core_register_libs
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.compat import string_types
from kivy.setupconfig import USE_SDL2
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')
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)))
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]
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]
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.'''
__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=False):
'''Indicate if the loader can save the Image object
.. versionchanged:: 1.11.0
Parameter `fmt` and `is_bytesio` 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 = []
@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
'''
# 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 = None
for loader in ImageLoader.loaders:
if (ext not in loader.extensions() or
not loader.can_load_memory()):
continue
Logger.debug('Image%s: Load <%s> from <%s>' %
(loader.__name__[11:], zfilename, filename))
try:
im = loader(zfilename, ext=ext, rawdata=tmpfile,
inline=True, **kwargs)
except:
# Loader failed, continue trying.
continue
break
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:
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):
ImageLoader.loaders.append(defcls)
@staticmethod
def load(filename, **kwargs):
# atlas ?
if filename[:8] == 'atlas://':
# remove the url
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])
# 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)
else:
im = None
for loader in ImageLoader.loaders:
if ext not in loader.extensions():
continue
Logger.debug(f'Image{loader.__name__[11:]}: Load <{filename}>')
im = loader(filename, **kwargs)
break
if im is None:
raise Exception(f'Unknown <{ext}> type, no loader found.')
return im
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.
'''
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
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, string_types):
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))
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)
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]
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
@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)
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)
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')
def load_memory(self, data, ext, filename='__inline__'):
'''(internal) Method to load an image from raw data.
'''
self._filename = filename
# see if there is a available loader for it
loaders = [loader for loader in ImageLoader.loaders if
loader.can_load_memory() and
ext in loader.extensions()]
if not loaders:
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)
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
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 = False
if isinstance(filename, BytesIO):
is_bytesio = True
if not fmt:
raise Exception(
"You must specify a format to save into a BytesIO 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=is_bytesio)
]
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
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
image_libs = []
if platform in ('macosx', 'ios'):
image_libs += [('imageio', 'img_imageio')]
image_libs += [
('tex', 'img_tex'),
('dds', 'img_dds')]
if USE_SDL2:
image_libs += [('sdl2', 'img_sdl2')]
else:
image_libs += [('pygame', 'img_pygame')]
image_libs += [
('ffpy', 'img_ffpyplayer'),
('pil', 'img_pil')]
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
@@ -0,0 +1,40 @@
'''
DDS: DDS image loader
'''
__all__ = ('ImageLoaderDDS', )
from kivy.lib.ddsfile import DDSFile
from kivy.logger import Logger
from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader
class ImageLoaderDDS(ImageLoaderBase):
@staticmethod
def extensions():
return ('dds', )
def load(self, filename):
try:
dds = DDSFile(filename=filename)
except:
Logger.warning('Image: Unable to load image <%s>' % filename)
raise
self.filename = filename
width, height = dds.size
im = ImageData(width, height, dds.dxt, dds.images[0], source=filename,
flip_vertical=False)
if len(dds.images) > 1:
images = dds.images
images_size = dds.images_size
for index in range(1, len(dds.images)):
w, h = images_size[index]
data = images[index]
im.add_mipmap(index, w, h, data)
return [im]
# register
ImageLoader.register(ImageLoaderDDS)
@@ -0,0 +1,87 @@
'''
FFPyPlayer: FFmpeg based image loader
'''
__all__ = ('ImageLoaderFFPy', )
import ffpyplayer
from ffpyplayer.pic import ImageLoader as ffImageLoader, SWScale
from ffpyplayer.tools import set_log_callback, get_log_callback
from kivy.logger import Logger
from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader
Logger.info('ImageLoaderFFPy: Using ffpyplayer {}'.format(ffpyplayer.version))
logger_func = {'quiet': Logger.critical, 'panic': Logger.critical,
'fatal': Logger.critical, 'error': Logger.error,
'warning': Logger.warning, 'info': Logger.info,
'verbose': Logger.debug, 'debug': Logger.debug}
def _log_callback(message, level):
message = message.strip()
if message:
logger_func[level]('ffpyplayer: {}'.format(message))
if not get_log_callback():
set_log_callback(_log_callback)
class ImageLoaderFFPy(ImageLoaderBase):
'''Image loader based on the ffpyplayer library.
.. versionadded:: 1.9.0
.. note:
This provider may support more formats than what is listed in
:meth:`extensions`.
'''
@staticmethod
def extensions():
'''Return accepted extensions for this loader'''
# See https://www.ffmpeg.org/general.html#Image-Formats
return ('bmp', 'dpx', 'exr', 'gif', 'ico', 'jpeg', 'jpg2000', 'jpg',
'jls', 'pam', 'pbm', 'pcx', 'pgm', 'pgmyuv', 'pic', 'png',
'ppm', 'ptx', 'sgi', 'ras', 'tga', 'tiff', 'webp', 'xbm',
'xface', 'xwd')
def load(self, filename):
try:
loader = ffImageLoader(filename)
except:
Logger.warning('Image: Unable to load image <%s>' % filename)
raise
# update internals
self.filename = filename
images = []
while True:
frame, t = loader.next_frame()
if frame is None:
break
images.append(frame)
if not len(images):
raise Exception('No image found in {}'.format(filename))
w, h = images[0].get_size()
ifmt = images[0].get_pixel_format()
if ifmt != 'rgba' and ifmt != 'rgb24':
fmt = 'rgba'
sws = SWScale(w, h, ifmt, ofmt=fmt)
for i, image in enumerate(images):
images[i] = sws.scale(image)
else:
fmt = ifmt if ifmt == 'rgba' else 'rgb'
return [ImageData(w, h, fmt, img.to_memoryview()[0], source_image=img)
for img in images]
# register
ImageLoader.register(ImageLoaderFFPy)
@@ -0,0 +1,123 @@
'''
PIL: PIL image loader
'''
__all__ = ('ImageLoaderPIL', )
try:
import Image as PILImage
except ImportError:
# for python3
from PIL import Image as PILImage
from kivy.logger import Logger
from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader
try:
# Pillow
PILImage.frombytes
PILImage.Image.tobytes
except AttributeError:
# PIL
# monkey patch frombytes and tobytes methods, refs:
# https://github.com/kivy/kivy/issues/5460
PILImage.frombytes = PILImage.frombuffer
PILImage.Image.tobytes = PILImage.Image.tostring
class ImageLoaderPIL(ImageLoaderBase):
'''Image loader based on the PIL library.
.. versionadded:: 1.0.8
Support for GIF animation added.
Gif animation has a lot of issues(transparency/color depths... etc).
In order to keep it simple, what is implemented here is what is
natively supported by the PIL library.
As a general rule, try to use gifs that have no transparency.
Gif's with transparency will work but be prepared for some
artifacts until transparency support is improved.
'''
@staticmethod
def can_save(fmt, is_bytesio):
if is_bytesio:
return False
return fmt in ImageLoaderPIL.extensions()
@staticmethod
def can_load_memory():
return True
@staticmethod
def extensions():
'''Return accepted extensions for this loader'''
PILImage.init()
return tuple((ext_with_dot[1:] for ext_with_dot in PILImage.EXTENSION))
def _img_correct(self, _img_tmp):
'''Convert image to the correct format and orientation.
'''
# image loader work only with rgb/rgba image
if _img_tmp.mode.lower() not in ('rgb', 'rgba'):
try:
imc = _img_tmp.convert('RGBA')
except:
Logger.warning(
'Image: Unable to convert image to rgba (was %s)' %
(_img_tmp.mode.lower()))
raise
_img_tmp = imc
return _img_tmp
def _img_read(self, im):
'''Read images from an animated file.
'''
im.seek(0)
# Read all images inside
try:
img_ol = None
while True:
img_tmp = im
img_tmp = self._img_correct(img_tmp)
if img_ol and (hasattr(im, 'dispose') and not im.dispose):
# paste new frame over old so as to handle
# transparency properly
img_ol.paste(img_tmp, (0, 0), img_tmp)
img_tmp = img_ol
img_ol = img_tmp
yield ImageData(img_tmp.size[0], img_tmp.size[1],
img_tmp.mode.lower(), img_tmp.tobytes())
im.seek(im.tell() + 1)
except EOFError:
pass
def load(self, filename):
try:
im = PILImage.open(filename)
except:
Logger.warning('Image: Unable to load image <%s>' % filename)
raise
# update internals
if not self._inline:
self.filename = filename
# returns an array of type ImageData len 1 if not a sequence image
return list(self._img_read(im))
@staticmethod
def save(filename, width, height, pixelfmt, pixels, flipped=False,
imagefmt=None):
image = PILImage.frombytes(pixelfmt.upper(), (width, height), pixels)
if flipped:
image = image.transpose(PILImage.FLIP_TOP_BOTTOM)
image.save(filename)
return True
# register
ImageLoader.register(ImageLoaderPIL)
@@ -0,0 +1,119 @@
'''
Pygame: Pygame image loader
.. warning::
Pygame has been deprecated and will be removed in the release after Kivy
1.11.0.
'''
__all__ = ('ImageLoaderPygame', )
from kivy.logger import Logger
from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader
from os.path import isfile
from kivy.utils import deprecated
try:
import pygame
except:
raise
class ImageLoaderPygame(ImageLoaderBase):
'''Image loader based on the PIL library'''
@deprecated(
msg='Pygame has been deprecated and will be removed after 1.11.0')
def __init__(self, *largs, **kwargs):
super(ImageLoaderPygame, self).__init__(*largs, **kwargs)
@staticmethod
def extensions():
'''Return accepted extensions for this loader'''
# under OS X, i got with "pygame.error: File is not a Windows BMP
# file". documentation said: The image module is a required dependency
# of Pygame, but it only optionally supports any extended file formats.
# By default it can only load uncompressed BMP image
if pygame.image.get_extended() == 0:
return ('bmp', )
return ('jpg', 'jpeg', 'jpe', 'png', 'bmp', 'pcx', 'tga', 'tiff',
'tif', 'lbm', 'pbm', 'ppm', 'xpm')
@staticmethod
def can_save(fmt, is_bytesio):
if is_bytesio:
return False
return fmt in ('png', 'jpg')
@staticmethod
def can_load_memory():
return True
def load(self, filename):
if not filename:
import traceback
traceback.print_stack()
return
try:
im = None
if self._inline:
im = pygame.image.load(filename, 'x.{}'.format(self._ext))
elif isfile(filename):
with open(filename, 'rb') as fd:
im = pygame.image.load(fd)
elif isinstance(filename, bytes):
try:
fname = filename.decode()
if isfile(fname):
with open(fname, 'rb') as fd:
im = pygame.image.load(fd)
except UnicodeDecodeError:
pass
if im is None:
im = pygame.image.load(filename)
except:
# Logger.warning(type(filename)('Image: Unable to load image <%s>')
# % filename)
raise
fmt = ''
if im.get_bytesize() == 3 and not im.get_colorkey():
fmt = 'rgb'
elif im.get_bytesize() == 4:
fmt = 'rgba'
# image loader work only with rgb/rgba image
if fmt not in ('rgb', 'rgba'):
try:
imc = im.convert(32)
fmt = 'rgba'
except:
try:
imc = im.convert_alpha()
fmt = 'rgba'
except:
Logger.warning(
'Image: Unable to convert image %r to rgba (was %r)' %
(filename, im.fmt))
raise
im = imc
# update internals
if not self._inline:
self.filename = filename
data = pygame.image.tostring(im, fmt.upper())
return [ImageData(im.get_width(), im.get_height(),
fmt, data, source=filename)]
@staticmethod
def save(filename, width, height, pixelfmt, pixels, flipped,
imagefmt=None):
surface = pygame.image.fromstring(
pixels, (width, height), pixelfmt.upper(), flipped)
pygame.image.save(surface, filename)
return True
# register
ImageLoader.register(ImageLoaderPygame)
@@ -0,0 +1,66 @@
'''
SDL2 image loader
=================
'''
__all__ = ('ImageLoaderSDL2', )
from kivy.logger import Logger
from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader
try:
from kivy.core.image import _img_sdl2
except ImportError:
from kivy.core import handle_win_lib_import_error
handle_win_lib_import_error(
'image', 'sdl2', 'kivy.core.image._img_sdl2')
raise
class ImageLoaderSDL2(ImageLoaderBase):
'''Image loader based on SDL2_image'''
def _ensure_ext(self):
_img_sdl2.init()
@staticmethod
def extensions():
'''Return accepted extensions for this loader'''
return ('bmp', 'jpg', 'jpeg', 'jpe', 'lbm', 'pcx', 'png', 'pnm',
'tga', 'tiff', 'webp', 'xcf', 'xpm', 'xv')
@staticmethod
def can_save(fmt, is_bytesio):
return fmt in ('jpg', 'png')
@staticmethod
def can_load_memory():
return True
def load(self, filename):
if self._inline:
data = filename.read()
info = _img_sdl2.load_from_memory(data)
else:
info = _img_sdl2.load_from_filename(filename)
if not info:
Logger.warning('Image: Unable to load image <%s>' % filename)
raise Exception('SDL2: Unable to load image')
w, h, fmt, pixels, rowlength = info
# update internals
if not self._inline:
self.filename = filename
return [ImageData(
w, h, fmt, pixels, source=filename,
rowlength=rowlength)]
@staticmethod
def save(filename, width, height, pixelfmt, pixels, flipped, imagefmt):
_img_sdl2.save(filename, width, height, pixelfmt, pixels, flipped,
imagefmt)
return True
# register
ImageLoader.register(ImageLoaderSDL2)
@@ -0,0 +1,58 @@
'''
Tex: Compressed texture
'''
__all__ = ('ImageLoaderTex', )
import json
from struct import unpack
from kivy.logger import Logger
from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader
class ImageLoaderTex(ImageLoaderBase):
@staticmethod
def extensions():
return ('tex', )
def load(self, filename):
try:
fd = open(filename, 'rb')
if fd.read(4) != 'KTEX':
raise Exception('Invalid tex identifier')
headersize = unpack('I', fd.read(4))[0]
header = fd.read(headersize)
if len(header) != headersize:
raise Exception('Truncated tex header')
info = json.loads(header)
data = fd.read()
if len(data) != info['datalen']:
raise Exception('Truncated tex data')
except:
Logger.warning('Image: Image <%s> is corrupted' % filename)
raise
width, height = info['image_size']
tw, th = info['texture_size']
images = [data]
im = ImageData(width, height, str(info['format']), images[0],
source=filename)
'''
if len(dds.images) > 1:
images = dds.images
images_size = dds.images_size
for index in range(1, len(dds.images)):
w, h = images_size[index]
data = images[index]
im.add_mipmap(index, w, h, data)
'''
return [im]
# register
ImageLoader.register(ImageLoaderTex)
@@ -0,0 +1,135 @@
'''
Spelling
========
Provides abstracted access to a range of spellchecking backends as well as
word suggestions. The API is inspired by enchant but other backends can be
added that implement the same API.
Spelling currently requires `python-enchant` for all platforms except
OSX, where a native implementation exists.
::
>>> from kivy.core.spelling import Spelling
>>> s = Spelling()
>>> s.list_languages()
['en', 'en_CA', 'en_GB', 'en_US']
>>> s.select_language('en_US')
>>> s.suggest('helo')
[u'hole', u'help', u'helot', u'hello', u'halo', u'hero', u'hell', u'held',
u'helm', u'he-lo']
'''
__all__ = ('Spelling', 'SpellingBase', 'NoSuchLangError',
'NoLanguageSelectedError')
import sys
from kivy.core import core_select_lib
class NoSuchLangError(Exception):
'''
Exception to be raised when a specific language could not be found.
'''
pass
class NoLanguageSelectedError(Exception):
'''
Exception to be raised when a language-using method is called but no
language was selected prior to the call.
'''
pass
class SpellingBase(object):
'''
Base class for all spelling providers.
Supports some abstract methods for checking words and getting suggestions.
'''
def __init__(self, language=None):
'''
If a `language` identifier (such as 'en_US') is provided and a matching
language exists, it is selected. If an identifier is provided and no
matching language exists, a NoSuchLangError exception is raised by
self.select_language().
If no `language` identifier is provided, we just fall back to the first
one that is available.
:Parameters:
`language`: str, defaults to None
If provided, indicates the language to be used. This needs
to be a language identifier understood by select_language(),
i.e. one of the options returned by list_languages().
If nothing is provided, the first available language is used.
If no language is available, NoLanguageSelectedError is raised.
'''
langs = self.list_languages()
try:
# If no language was specified, we just use the first one
# that is available.
fallback_lang = langs[0]
except IndexError:
raise NoLanguageSelectedError("No languages available!")
self.select_language(language or fallback_lang)
def select_language(self, language):
'''
From the set of registered languages, select the first language
for `language`.
:Parameters:
`language`: str
Language identifier. Needs to be one of the options returned by
list_languages(). Sets the language used for spell checking and
word suggestions.
'''
raise NotImplementedError('select_language() method not implemented '
'by abstract spelling base class!')
def list_languages(self):
'''
Return a list of all supported languages.
E.g. ['en', 'en_GB', 'en_US', 'de', ...]
'''
raise NotImplementedError('list_languages() is not implemented '
'by abstract spelling base class!')
def check(self, word):
'''
If `word` is a valid word in `self._language` (the currently active
language), returns True. If the word shouldn't be checked, returns
None (e.g. for ''). If it is not a valid word in `self._language`,
return False.
:Parameters:
`word`: str
The word to check.
'''
raise NotImplementedError('check() not implemented by abstract ' +
'spelling base class!')
def suggest(self, fragment):
'''
For a given `fragment` (i.e. part of a word or a word by itself),
provide corrections (`fragment` may be misspelled) or completions
as a list of strings.
:Parameters:
`fragment`: str
The word fragment to get suggestions/corrections for.
E.g. 'foo' might become 'of', 'food' or 'foot'.
'''
raise NotImplementedError('suggest() not implemented by abstract ' +
'spelling base class!')
_libs = (('enchant', 'spelling_enchant', 'SpellingEnchant'), )
if sys.platform == 'darwin':
_libs += (('osxappkit', 'spelling_osxappkit', 'SpellingOSXAppKit'), )
Spelling = core_select_lib('spelling', _libs)
@@ -0,0 +1,50 @@
'''
Enchant Spelling
================
Implementation spelling backend based on enchant.
.. warning:: pyenchant doesn't have dedicated build anymore for Windows/x64.
See https://github.com/kivy/kivy/issues/5816 for more information
'''
import enchant
from kivy.core.spelling import SpellingBase, NoSuchLangError
from kivy.compat import PY2
class SpellingEnchant(SpellingBase):
'''
Spelling backend based on the enchant library.
'''
def __init__(self, language=None):
self._language = None
super(SpellingEnchant, self).__init__(language)
def select_language(self, language):
try:
self._language = enchant.Dict(language)
except enchant.DictNotFoundError:
err = 'Enchant Backend: No language for "%s"' % (language, )
raise NoSuchLangError(err)
def list_languages(self):
# Note: We do NOT return enchant.list_dicts because that also returns
# the enchant dict objects and not only the language identifiers.
return enchant.list_languages()
def check(self, word):
if not word:
return None
return self._language.check(word)
def suggest(self, fragment):
suggestions = self._language.suggest(fragment)
# Don't show suggestions that are invalid
suggestions = [s for s in suggestions if self.check(s)]
if PY2:
suggestions = [s.decode('utf-8') for s in suggestions]
return suggestions
@@ -0,0 +1,64 @@
'''
AppKit Spelling: Implements spelling backend based on OSX's spellchecking
features provided by the ApplicationKit.
NOTE:
Requires pyobjc and setuptools to be installed!
`sudo easy_install pyobjc setuptools`
Developers should read:
http://developer.apple.com/mac/library/documentation/
Cocoa/Conceptual/SpellCheck/SpellCheck.html
http://developer.apple.com/cocoa/pyobjc.html
'''
from AppKit import NSSpellChecker, NSMakeRange
from kivy.core.spelling import SpellingBase, NoSuchLangError
class SpellingOSXAppKit(SpellingBase):
'''
Spelling backend based on OSX's spelling features provided by AppKit.
'''
def __init__(self, language=None):
self._language = NSSpellChecker.alloc().init()
super(SpellingOSXAppKit, self).__init__(language)
def select_language(self, language):
success = self._language.setLanguage_(language)
if not success:
err = 'AppKit Backend: No language "%s" ' % (language, )
raise NoSuchLangError(err)
def list_languages(self):
return list(self._language.availableLanguages())
def check(self, word):
# TODO Implement this!
# NSSpellChecker provides several functions that look like what we
# need, but they're a) slooow and b) return a strange result.
# Might be a snow leopard bug. Have to test further.
# See: http://paste.pocoo.org/show/217968/
if not word:
return None
err = 'check() not currently supported by the OSX AppKit backend'
raise NotImplementedError(err)
def suggest(self, fragment):
l = self._language
# XXX Both ways below work on OSX 10.6. It has not been tested on any
# other version, but it should work.
try:
# This is deprecated as of OSX 10.6, hence the try-except
return list(l.guessesForWord_(fragment))
except AttributeError:
# From 10.6 onwards you're supposed to do it like this:
checkrange = NSMakeRange(0, len(fragment))
g = l.\
guessesForWordRange_inString_language_inSpellDocumentWithTag_(
checkrange, fragment, l.language(), 0)
# Right, this was much easier, Apple! :-)
return list(g)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,893 @@
'''
Text Markup
===========
.. versionadded:: 1.1.0
.. versionchanged:: 1.10.1
Added `font_context`, `font_features` and `text_language` (Pango only)
We provide a simple text-markup for inline text styling. The syntax look the
same as the `BBCode <http://en.wikipedia.org/wiki/BBCode>`_.
A tag is defined as ``[tag]``, and should have a corresponding
``[/tag]`` closing tag. For example::
[b]Hello [color=ff0000]world[/color][/b]
The following tags are available:
``[b][/b]``
Activate bold text
``[i][/i]``
Activate italic text
``[u][/u]``
Underlined text
``[s][/s]``
Strikethrough text
``[font=<str>][/font]``
Change the font (note: this refers to a TTF file or registered alias)
``[font_context=<str>][/font_context]``
Change context for the font, use string value "none" for isolated context.
``[font_family=<str>][/font_family]``
Font family to request for drawing. This is only valid when using a
font context, and takes precedence over `[font]`. See
:class:`kivy.uix.label.Label` for details.
``[font_features=<str>][/font_features]``
OpenType font features, in CSS format, this is passed straight
through to Pango. The effects of requesting a feature depends on loaded
fonts, library versions, etc. Pango only, requires v1.38 or later.
``[size=<size>][/size]``
Change the font size. <size> should be an integer, optionally with a
unit (i.e. ``16sp``)
``[color=#<color>][/color]``
Change the text color
``[ref=<str>][/ref]``
Add an interactive zone. The reference + all the word box inside the
reference will be available in :attr:`MarkupLabel.refs`
``[anchor=<str>]``
Put an anchor in the text. You can get the position of your anchor within
the text with :attr:`MarkupLabel.anchors`
``[sub][/sub]``
Display the text at a subscript position relative to the text before it.
``[sup][/sup]``
Display the text at a superscript position relative to the text before it.
``[text_language=<str>][/text_language]``
Language of the text, this is an RFC-3066 format language tag (as string),
for example "en_US", "zh_CN", "fr" or "ja". This can impact font selection,
metrics and rendering. For example, the same bytes of text can look
different for `ur` and `ar` languages, though both use Arabic script.
Use the string `'none'` to revert to locale detection. Pango only.
If you need to escape the markup from the current text, use
:func:`kivy.utils.escape_markup`.
'''
__all__ = ('MarkupLabel', )
import re
from kivy.properties import dpi2px
from kivy.parser import parse_color
from kivy.logger import Logger
from kivy.core.text import Label, LabelBase
from kivy.core.text.text_layout import layout_text, LayoutWord, LayoutLine
from copy import copy
from functools import partial
# We need to do this trick when documentation is generated
MarkupLabelBase = Label
if Label is None:
MarkupLabelBase = LabelBase
class MarkupLabel(MarkupLabelBase):
'''Markup text label.
See module documentation for more information.
'''
def __init__(self, *largs, **kwargs):
self._style_stack = {}
self._refs = {}
self._anchors = {}
super(MarkupLabel, self).__init__(*largs, **kwargs)
self._internal_size = 0, 0
self._cached_lines = []
@property
def refs(self):
'''Get the bounding box of all the ``[ref=...]``::
{ 'refA': ((x1, y1, x2, y2), (x1, y1, x2, y2)), ... }
'''
return self._refs
@property
def anchors(self):
'''Get the position of all the ``[anchor=...]``::
{ 'anchorA': (x, y), 'anchorB': (x, y), ... }
'''
return self._anchors
@property
def markup(self):
'''Return the text with all the markup split::
>>> MarkupLabel('[b]Hello world[/b]').markup
>>> ('[b]', 'Hello world', '[/b]')
'''
s = re.split(r'(\[.*?\])', self.label)
s = [x for x in s if x != '']
return s
def _push_style(self, k):
if k not in self._style_stack:
self._style_stack[k] = []
self._style_stack[k].append(self.options[k])
def _pop_style(self, k):
if k not in self._style_stack or len(self._style_stack[k]) == 0:
Logger.warning('Label: pop style stack without push')
return
v = self._style_stack[k].pop()
self.options[k] = v
def render(self, real=False):
options = copy(self.options)
if not real:
ret = self._pre_render()
else:
ret = self._render_real()
self.options = options
return ret
def _pre_render(self):
# split markup, words, and lines
# result: list of word with position and width/height
# during the first pass, we don't care about h/valign
self._cached_lines = lines = []
self._refs = {}
self._anchors = {}
clipped = False
w = h = 0
uw, uh = self.text_size
spush = self._push_style
spop = self._pop_style
options = self.options
options['_ref'] = None
options['_anchor'] = None
options['script'] = 'normal'
shorten = options['shorten']
# if shorten, then don't split lines to fit uw, because it will be
# flattened later when shortening and broken up lines if broken
# mid-word will have space mid-word when lines are joined
uw_temp = None if shorten else uw
xpad = options['padding'][0] + options['padding'][2]
uhh = (None if uh is not None and options['valign'] != 'top' or
options['shorten'] else uh)
options['strip'] = options['strip'] or options['halign'] == 'justify'
find_base_dir = Label.find_base_direction
base_dir = options['base_direction']
self._resolved_base_dir = None
for item in self.markup:
if item == '[b]':
spush('bold')
options['bold'] = True
self.resolve_font_name()
elif item == '[/b]':
spop('bold')
self.resolve_font_name()
elif item == '[i]':
spush('italic')
options['italic'] = True
self.resolve_font_name()
elif item == '[/i]':
spop('italic')
self.resolve_font_name()
elif item == '[u]':
spush('underline')
options['underline'] = True
self.resolve_font_name()
elif item == '[/u]':
spop('underline')
self.resolve_font_name()
elif item == '[s]':
spush('strikethrough')
options['strikethrough'] = True
self.resolve_font_name()
elif item == '[/s]':
spop('strikethrough')
self.resolve_font_name()
elif item[:6] == '[size=':
item = item[6:-1]
try:
if item[-2:] in ('px', 'pt', 'in', 'cm', 'mm', 'dp', 'sp'):
size = dpi2px(item[:-2], item[-2:])
else:
size = int(item)
except ValueError:
raise
size = options['font_size']
spush('font_size')
options['font_size'] = size
elif item == '[/size]':
spop('font_size')
elif item[:7] == '[color=':
color = parse_color(item[7:-1])
spush('color')
options['color'] = color
elif item == '[/color]':
spop('color')
elif item[:6] == '[font=':
fontname = item[6:-1]
spush('font_name')
options['font_name'] = fontname
self.resolve_font_name()
elif item == '[/font]':
spop('font_name')
self.resolve_font_name()
elif item[:13] == '[font_family=':
spush('font_family')
options['font_family'] = item[13:-1]
elif item == '[/font_family]':
spop('font_family')
elif item[:14] == '[font_context=':
fctx = item[14:-1]
if not fctx or fctx.lower() == 'none':
fctx = None
spush('font_context')
options['font_context'] = fctx
elif item == '[/font_context]':
spop('font_context')
elif item[:15] == '[font_features=':
spush('font_features')
options['font_features'] = item[15:-1]
elif item == '[/font_features]':
spop('font_features')
elif item[:15] == '[text_language=':
lang = item[15:-1]
if not lang or lang.lower() == 'none':
lang = None
spush('text_language')
options['text_language'] = lang
elif item == '[/text_language]':
spop('text_language')
elif item[:5] == '[sub]':
spush('font_size')
spush('script')
options['font_size'] = options['font_size'] * .5
options['script'] = 'subscript'
elif item == '[/sub]':
spop('font_size')
spop('script')
elif item[:5] == '[sup]':
spush('font_size')
spush('script')
options['font_size'] = options['font_size'] * .5
options['script'] = 'superscript'
elif item == '[/sup]':
spop('font_size')
spop('script')
elif item[:5] == '[ref=':
ref = item[5:-1]
spush('_ref')
options['_ref'] = ref
elif item == '[/ref]':
spop('_ref')
elif not clipped and item[:8] == '[anchor=':
options['_anchor'] = item[8:-1]
elif not clipped:
item = item.replace('&bl;', '[').replace(
'&br;', ']').replace('&amp;', '&')
if not base_dir:
base_dir = self._resolved_base_dir = find_base_dir(item)
opts = copy(options)
extents = self.get_cached_extents()
opts['space_width'] = extents(' ')[0]
w, h, clipped = layout_text(
item, lines, (w, h), (uw_temp, uhh),
opts, extents,
append_down=True,
complete=False
)
if len(lines): # remove any trailing spaces from the last line
old_opts = self.options
self.options = copy(opts)
w, h, clipped = layout_text(
'', lines, (w, h), (uw_temp, uhh),
self.options, self.get_cached_extents(),
append_down=True,
complete=True
)
self.options = old_opts
self.is_shortened = False
if shorten:
options['_ref'] = None # no refs for you!
options['_anchor'] = None
w, h, lines = self.shorten_post(lines, w, h)
self._cached_lines = lines
# when valign is not top, for markup we layout everything (text_size[1]
# is temporarily set to None) and after layout cut to size if too tall
elif uh != uhh and h > uh and len(lines) > 1:
if options['valign'] == 'bottom':
i = 0
while i < len(lines) - 1 and h > uh:
h -= lines[i].h
i += 1
del lines[:i]
else: # middle
i = 0
top = int(h / 2. + uh / 2.) # remove extra top portion
while i < len(lines) - 1 and h > top:
h -= lines[i].h
i += 1
del lines[:i]
i = len(lines) - 1 # remove remaining bottom portion
while i and h > uh:
h -= lines[i].h
i -= 1
del lines[i + 1:]
# now justify the text
if options['halign'] == 'justify' and uw is not None:
# XXX: update refs to justified pos
# when justify, each line should've been stripped already
split = partial(re.split, re.compile('( +)'))
uww = uw - xpad
chr = type(self.text)
space = chr(' ')
empty = chr('')
for i in range(len(lines)):
line = lines[i]
words = line.words
# if there's nothing to justify, we're done
if (not line.w or int(uww - line.w) <= 0 or not len(words) or
line.is_last_line):
continue
done = False
parts = [None, ] * len(words) # contains words split by space
idxs = [None, ] * len(words) # indices of the space in parts
# break each word into spaces and add spaces until it's full
# do first round of split in case we don't need to split all
for w in range(len(words)):
word = words[w]
sw = word.options['space_width']
p = parts[w] = split(word.text)
idxs[w] = [v for v in range(len(p)) if
p[v].startswith(' ')]
# now we have the indices of the spaces in split list
for k in idxs[w]:
# try to add single space at each space
if line.w + sw > uww:
done = True
break
line.w += sw
word.lw += sw
p[k] += space
if done:
break
# there's not a single space in the line?
if not any(idxs):
continue
# now keep adding spaces to already split words until done
while not done:
for w in range(len(words)):
if not idxs[w]:
continue
word = words[w]
sw = word.options['space_width']
p = parts[w]
for k in idxs[w]:
# try to add single space at each space
if line.w + sw > uww:
done = True
break
line.w += sw
word.lw += sw
p[k] += space
if done:
break
# if not completely full, push last words to right edge
diff = int(uww - line.w)
if diff > 0:
# find the last word that had a space
for w in range(len(words) - 1, -1, -1):
if not idxs[w]:
continue
break
old_opts = self.options
self.options = word.options
word = words[w]
# split that word into left/right and push right till uww
l_text = empty.join(parts[w][:idxs[w][-1]])
r_text = empty.join(parts[w][idxs[w][-1]:])
left = LayoutWord(
word.options,
self.get_extents(l_text)[0],
word.lh,
l_text
)
right = LayoutWord(
word.options,
self.get_extents(r_text)[0],
word.lh,
r_text
)
left.lw = max(left.lw, word.lw + diff - right.lw)
self.options = old_opts
# now put words back together with right/left inserted
for k in range(len(words)):
if idxs[k]:
words[k].text = empty.join(parts[k])
words[w] = right
words.insert(w, left)
else:
for k in range(len(words)):
if idxs[k]:
words[k].text = empty.join(parts[k])
line.w = uww
w = max(w, uww)
self._internal_size = w, h
if uw:
w = uw
if uh:
h = uh
if h > 1 and w < 2:
w = 2
if w < 1:
w = 1
if h < 1:
h = 1
return int(w), int(h)
def render_lines(self, lines, options, render_text, y, size):
padding_left = options['padding'][0]
padding_right = options['padding'][2]
w = size[0]
halign = options['halign']
refs = self._refs
anchors = self._anchors
base_dir = options['base_direction'] or self._resolved_base_dir
auto_halign_r = halign == 'auto' and base_dir and 'rtl' in base_dir
for layout_line in lines: # for plain label each line has only one str
lw, lh = layout_line.w, layout_line.h
x = padding_left
if halign == 'center':
x = min(
int(w - lw),
max(
int(padding_left),
int((w - lw + padding_left - padding_right) / 2.0)
)
)
elif halign == 'right' or auto_halign_r:
x = max(0, int(w - lw - padding_right))
layout_line.x = x
layout_line.y = y
psp = pph = 0
for word in layout_line.words:
options = self.options = word.options
# the word height is not scaled by line_height, only lh was
wh = options['line_height'] * word.lh
# calculate sub/super script pos
if options['script'] == 'superscript':
script_pos = max(0, psp if psp and (wh < pph)
else self.get_descent())
psp = script_pos
pph = wh
elif options['script'] == 'subscript':
script_pos = min(lh - wh, ((psp + pph) - wh)
if pph and (wh < pph) else (lh - wh))
pph = wh
psp = script_pos
else:
script_pos = (lh - wh) / 1.25
psp = pph = 0
if len(word.text):
render_text(word.text, x, y + script_pos)
# should we record refs ?
ref = options['_ref']
if ref is not None:
if ref not in refs:
refs[ref] = []
refs[ref].append((x, y, x + word.lw, y + wh))
# Should we record anchors?
anchor = options['_anchor']
if anchor is not None:
if anchor not in anchors:
anchors[anchor] = (x, y)
x += word.lw
y += lh
return y
def shorten_post(self, lines, w, h, margin=2):
''' Shortens the text to a single line according to the label options.
This function operates on a text that has already been laid out because
for markup, parts of text can have different size and options.
If :attr:`text_size` [0] is None, the lines are returned unchanged.
Otherwise, the lines are converted to a single line fitting within the
constrained width, :attr:`text_size` [0].
:params:
`lines`: list of `LayoutLine` instances describing the text.
`w`: int, the width of the text in lines, including padding.
`h`: int, the height of the text in lines, including padding.
`margin` int, the additional space left on the sides. This is in
addition to :attr:`padding_x`.
:returns:
3-tuple of (xw, h, lines), where w, and h is similar to the input
and contains the resulting width / height of the text, including
padding. lines, is a list containing a single `LayoutLine`, which
contains the words for the line.
'''
def n(line, c):
''' A function similar to text.find, except it's an iterator that
returns successive occurrences of string c in list line. line is
not a string, but a list of LayoutWord instances that we walk
from left to right returning the indices of c in the words as we
encounter them. Note that the options can be different among the
words.
:returns:
3-tuple: the index of the word in line, the index of the
occurrence in word, and the extents (width) of the combined
words until this occurrence, not including the occurrence char.
If no more are found it returns (-1, -1, total_w) where total_w
is the full width of all the words.
'''
total_w = 0
for w in range(len(line)):
word = line[w]
if not word.lw:
continue
f = partial(word.text.find, c)
i = f()
while i != -1:
self.options = word.options
yield w, i, total_w + self.get_extents(word.text[:i])[0]
i = f(i + 1)
self.options = word.options
total_w += self.get_extents(word.text)[0]
yield -1, -1, total_w # this should never be reached, really
def p(line, c):
''' Similar to the `n` function, except it returns occurrences of c
from right to left in the list, line, similar to rfind.
'''
total_w = 0
offset = 0 if len(c) else 1
for w in range(len(line) - 1, -1, -1):
word = line[w]
if not word.lw:
continue
f = partial(word.text.rfind, c)
i = f()
while i != -1:
self.options = word.options
yield (w, i, total_w +
self.get_extents(word.text[i + 1:])[0])
if i:
i = f(0, i - offset)
else:
if not c:
self.options = word.options
yield (w, -1, total_w +
self.get_extents(word.text)[0])
break
self.options = word.options
total_w += self.get_extents(word.text)[0]
yield -1, -1, total_w # this should never be reached, really
def n_restricted(line, uw, c):
''' Similar to the function `n`, except it only returns the first
occurrence and it's not an iterator. Furthermore, if the first
occurrence doesn't fit within width uw, it returns the index of
whatever amount of text will still fit in uw.
:returns:
similar to the function `n`, except it's a 4-tuple, with the
last element a boolean, indicating if we had to clip the text
to fit in uw (True) or if the whole text until the first
occurrence fitted in uw (False).
'''
total_w = 0
if not len(line):
return 0, 0, 0
for w in range(len(line)):
word = line[w]
f = partial(word.text.find, c)
self.options = word.options
extents = self.get_cached_extents()
i = f()
if i != -1:
ww = extents(word.text[:i])[0]
if i != -1 and total_w + ww <= uw: # found and it fits
return w, i, total_w + ww, False
elif i == -1:
ww = extents(word.text)[0]
if total_w + ww <= uw: # wasn't found and all fits
total_w += ww
continue
i = len(word.text)
# now just find whatever amount of the word does fit
e = 0
while e != i and total_w + extents(word.text[:e])[0] <= uw:
e += 1
e = max(0, e - 1)
return w, e, total_w + extents(word.text[:e])[0], True
return -1, -1, total_w, False
def p_restricted(line, uw, c):
''' Similar to `n_restricted`, except it returns the first
occurrence starting from the right, like `p`.
'''
total_w = 0
if not len(line):
return 0, 0, 0
for w in range(len(line) - 1, -1, -1):
word = line[w]
f = partial(word.text.rfind, c)
self.options = word.options
extents = self.get_cached_extents()
i = f()
if i != -1:
ww = extents(word.text[i + 1:])[0]
if i != -1 and total_w + ww <= uw: # found and it fits
return w, i, total_w + ww, False
elif i == -1:
ww = extents(word.text)[0]
if total_w + ww <= uw: # wasn't found and all fits
total_w += ww
continue
# now just find whatever amount of the word does fit
s = len(word.text) - 1
while s >= 0 and total_w + extents(word.text[s:])[0] <= uw:
s -= 1
return w, s, total_w + extents(word.text[s + 1:])[0], True
return -1, -1, total_w, False
textwidth = self.get_cached_extents()
uw = self.text_size[0]
if uw is None:
return w, h, lines
old_opts = copy(self.options)
uw = max(
0,
int(uw - old_opts["padding"][0] - old_opts["padding"][2] - margin),
)
chr = type(self.text)
ssize = textwidth(' ')
c = old_opts['split_str']
line_height = old_opts['line_height']
xpad, ypad = (
old_opts["padding"][0] + old_opts["padding"][2],
old_opts["padding"][1] + old_opts["padding"][3],
)
dir = old_opts['shorten_from'][0]
# flatten lines into single line
line = []
last_w = 0
for l in range(len(lines)):
# concatenate (non-empty) inside lines with a space
this_line = lines[l]
if last_w and this_line.w and not this_line.line_wrap:
line.append(LayoutWord(old_opts, ssize[0], ssize[1], chr(' ')))
last_w = this_line.w or last_w
for word in this_line.words:
if word.lw:
line.append(word)
# if that fits, just return the flattened line
lw = sum([word.lw for word in line])
if lw <= uw:
lh = max([word.lh for word in line] + [0]) * line_height
self.is_shortened = False
return (
lw + xpad,
lh + ypad,
[LayoutLine(0, 0, lw, lh, 1, 0, line)]
)
elps_opts = copy(old_opts)
if 'ellipsis_options' in old_opts:
elps_opts.update(old_opts['ellipsis_options'])
# Set new opts for ellipsis
self.options = elps_opts
# find the size of ellipsis that'll fit
elps_s = textwidth('...')
if elps_s[0] > uw: # even ellipsis didn't fit...
self.is_shortened = True
s = textwidth('..')
if s[0] <= uw:
return (
s[0] + xpad,
s[1] * line_height + ypad,
[LayoutLine(
0, 0, s[0], s[1], 1, 0,
[LayoutWord(old_opts, s[0], s[1], '..')])]
)
else:
s = textwidth('.')
return (
s[0] + xpad,
s[1] * line_height + ypad,
[LayoutLine(
0, 0, s[0], s[1], 1, 0,
[LayoutWord(old_opts, s[0], s[1], '.')])]
)
elps = LayoutWord(elps_opts, elps_s[0], elps_s[1], '...')
uw -= elps_s[0]
# Restore old opts
self.options = old_opts
# now find the first left and right words that fit
w1, e1, l1, clipped1 = n_restricted(line, uw, c)
w2, s2, l2, clipped2 = p_restricted(line, uw, c)
if dir != 'l': # center or right
line1 = None
if clipped1 or clipped2 or l1 + l2 > uw:
# if either was clipped or both don't fit, just take first
if len(c):
self.options = old_opts
old_opts['split_str'] = ''
res = self.shorten_post(lines, w, h, margin)
self.options['split_str'] = c
self.is_shortened = True
return res
line1 = line[:w1]
last_word = line[w1]
last_text = last_word.text[:e1]
self.options = last_word.options
s = self.get_extents(last_text)
line1.append(LayoutWord(last_word.options, s[0], s[1],
last_text))
elif (w1, e1) == (-1, -1): # this shouldn't occur
line1 = line
if line1:
line1.append(elps)
lw = sum([word.lw for word in line1])
lh = max([word.lh for word in line1]) * line_height
self.options = old_opts
self.is_shortened = True
return (
lw + xpad,
lh + ypad,
[LayoutLine(0, 0, lw, lh, 1, 0, line1)]
)
# now we know that both the first and last word fit, and that
# there's at least one instances of the split_str in the line
if (w1, e1) != (w2, s2): # more than one split_str
if dir == 'r':
f = n(line, c) # iterator
assert next(f)[:-1] == (w1, e1) # first word should match
ww1, ee1, l1 = next(f)
while l2 + l1 <= uw:
w1, e1 = ww1, ee1
ww1, ee1, l1 = next(f)
if (w1, e1) == (w2, s2):
break
else: # center
f = n(line, c) # iterator
f_inv = p(line, c) # iterator
assert next(f)[:-1] == (w1, e1)
assert next(f_inv)[:-1] == (w2, s2)
while True:
if l1 <= l2:
ww1, ee1, l1 = next(f) # hypothesize that next fit
if l2 + l1 > uw:
break
w1, e1 = ww1, ee1
if (w1, e1) == (w2, s2):
break
else:
ww2, ss2, l2 = next(f_inv)
if l2 + l1 > uw:
break
w2, s2 = ww2, ss2
if (w1, e1) == (w2, s2):
break
else: # left
line1 = [elps]
if clipped1 or clipped2 or l1 + l2 > uw:
# if either was clipped or both don't fit, just take last
if len(c):
self.options = old_opts
old_opts['split_str'] = ''
res = self.shorten_post(lines, w, h, margin)
self.options['split_str'] = c
self.is_shortened = True
return res
first_word = line[w2]
first_text = first_word.text[s2 + 1:]
self.options = first_word.options
s = self.get_extents(first_text)
line1.append(LayoutWord(first_word.options, s[0], s[1],
first_text))
line1.extend(line[w2 + 1:])
elif (w1, e1) == (-1, -1): # this shouldn't occur
line1 = line
if len(line1) != 1:
lw = sum([word.lw for word in line1])
lh = max([word.lh for word in line1]) * line_height
self.options = old_opts
self.is_shortened = True
return (
lw + xpad,
lh + ypad,
[LayoutLine(0, 0, lw, lh, 1, 0, line1)]
)
# now we know that both the first and last word fit, and that
# there's at least one instances of the split_str in the line
if (w1, e1) != (w2, s2): # more than one split_str
f_inv = p(line, c) # iterator
assert next(f_inv)[:-1] == (w2, s2) # last word should match
ww2, ss2, l2 = next(f_inv)
while l2 + l1 <= uw:
w2, s2 = ww2, ss2
ww2, ss2, l2 = next(f_inv)
if (w1, e1) == (w2, s2):
break
# now add back the left half
line1 = line[:w1]
last_word = line[w1]
last_text = last_word.text[:e1]
self.options = last_word.options
s = self.get_extents(last_text)
if len(last_text):
line1.append(LayoutWord(last_word.options, s[0], s[1], last_text))
line1.append(elps)
# now add back the right half
first_word = line[w2]
first_text = first_word.text[s2 + 1:]
self.options = first_word.options
s = self.get_extents(first_text)
if len(first_text):
line1.append(LayoutWord(first_word.options, s[0], s[1],
first_text))
line1.extend(line[w2 + 1:])
lw = sum([word.lw for word in line1])
lh = max([word.lh for word in line1]) * line_height
self.options = old_opts
if uw < lw:
self.is_shortened = True
return (
lw + xpad,
lh + ypad,
[LayoutLine(0, 0, lw, lh, 1, 0, line1)]
)
@@ -0,0 +1,13 @@
cdef class LayoutWord:
cdef public object text
cdef public int lw, lh
cdef public dict options
cdef class LayoutLine:
cdef public int x, y, w, h
cdef public int line_wrap # whether this line wraps from last line
cdef public int is_last_line # in a paragraph
cdef public list words
@@ -0,0 +1,145 @@
'''
Pango text provider
===================
.. versionadded:: 1.11.0
.. warning::
The low-level Pango API is experimental, and subject to change without
notice for as long as this warning is present.
Installation
------------
1. Install pangoft2 (`apt install libfreetype6-dev libpango1.0-dev
libpangoft2-1.0-0`) or ensure it is available in pkg-config
2. Recompile kivy. Check that pangoft2 is found `use_pangoft2 = 1`
3. Test it! Enforce the text core renderer to pango using environment variable:
`export KIVY_TEXT=pango`
This has been tested on OSX and Linux, Python 3.6.
Font context types for FontConfig+FreeType2 backend
---------------------------------------------------
* `system://` - `FcInitLoadConfigAndFonts()`
* `systemconfig://` - `FcInitLoadConfig()`
* `directory://<PATH>` - `FcInitLoadConfig()` + `FcAppFontAddDir()`
* `fontconfig://<PATH>` - `FcConfigCreate()` + `FcConfigParseAndLoad()`
* Any other context name - `FcConfigCreate()`
Low-level Pango access
----------------------
Since Kivy currently does its own text layout, the Label and TextInput widgets
do not take full advantage of Pango. For example, line breaks do not take
language/script into account, and switching alignment per paragraph (for bi-
directional text) is not supported. For advanced i18n requirements, we provide
a simple wrapper around PangoLayout that you can use to render text.
* https://developer.gnome.org/pango/1.40/pango-Layout-Objects.html
* https://developer.gnome.org/pango/1.40/PangoMarkupFormat.html
* See the `kivy/core/text/_text_pango.pyx` file @ `cdef class KivyPangoLayout`
for more information. Not all features of PangoLayout are implemented.
.. python::
from kivy.core.window import Window # OpenGL must be initialized
from kivy.core.text._text_pango import KivyPangoLayout
layout = KivyPangoLayout('system://')
layout.set_markup('<span font="20">Hello <b>World!</b></span>')
tex = layout.render_as_Texture()
Known limitations
-----------------
* Pango versions older than v1.38 has not been tested. It may work on
some systems with older pango and newer FontConfig/FreeType2 versions.
* Kivy's text layout is used, not Pango. This means we do not use Pango's
line-breaking feature (which is superior to Kivy's), and we can't use
Pango's bidirectional cursor helpers in TextInput.
* Font family collisions can happen. For example, if you use a `system://`
context and add a custom `Arial.ttf`, using `arial` as the `font_family`
may or may not draw with your custom font (depending on whether or not
there is already a system-wide "arial" font installed)
* Rendering is inefficient; the normal way to integrate Pango would be
using a dedicated PangoLayout per widget. This is not currently practical
due to missing abstractions in Kivy core (in the current implementation,
we have a dedicated PangoLayout *per font context,* which is rendered
once for each LayoutWord)
'''
__all__ = ('LabelPango', )
from types import MethodType
from os.path import isfile
from kivy.resources import resource_find
from kivy.core.text import LabelBase, FontContextManagerBase
from kivy.core.text._text_pango import (
KivyPangoRenderer,
kpango_get_extents,
kpango_get_ascent,
kpango_get_descent,
kpango_find_base_dir,
kpango_font_context_exists,
kpango_font_context_create,
kpango_font_context_destroy,
kpango_font_context_add_font,
kpango_font_context_list,
kpango_font_context_list_custom,
kpango_font_context_list_families)
class LabelPango(LabelBase):
_font_family_support = True
def __init__(self, *largs, **kwargs):
self.get_extents = MethodType(kpango_get_extents, self)
self.get_ascent = MethodType(kpango_get_ascent, self)
self.get_descent = MethodType(kpango_get_descent, self)
super(LabelPango, self).__init__(*largs, **kwargs)
find_base_direction = staticmethod(kpango_find_base_dir)
def _render_begin(self):
self._rdr = KivyPangoRenderer(*self._size)
def _render_text(self, text, x, y):
self._rdr.render(self, text, x, y)
def _render_end(self):
imgdata = self._rdr.get_ImageData()
del self._rdr
return imgdata
class PangoFontContextManager(FontContextManagerBase):
create = staticmethod(kpango_font_context_create)
exists = staticmethod(kpango_font_context_exists)
destroy = staticmethod(kpango_font_context_destroy)
list = staticmethod(kpango_font_context_list)
list_families = staticmethod(kpango_font_context_list_families)
list_custom = staticmethod(kpango_font_context_list_custom)
@staticmethod
def add_font(font_context, filename, autocreate=True, family=None):
if not autocreate and not PangoFontContextManager.exists(font_context):
raise Exception("FontContextManager: Attempt to add font file "
"'{}' to non-existing context '{}' without "
"autocreate.".format(filename, font_context))
if not filename:
raise Exception("FontContextManager: Cannot add empty font file")
if not isfile(filename):
filename = resource_find(filename)
if not isfile(filename):
if not filename.endswith('.ttf'):
filename = resource_find('{}.ttf'.format(filename))
if filename and isfile(filename):
return kpango_font_context_add_font(font_context, filename)
raise Exception("FontContextManager: Attempt to add non-existent "
"font file: '{}' to context '{}'"
.format(filename, font_context))
@@ -0,0 +1,84 @@
'''
Text PIL: Draw text with PIL
'''
__all__ = ('LabelPIL', )
from PIL import Image, ImageFont, ImageDraw
from kivy.compat import text_type
from kivy.core.text import LabelBase
from kivy.core.image import ImageData
# used for fetching extends before creature image surface
default_font = ImageFont.load_default()
class LabelPIL(LabelBase):
_cache = {}
def _select_font(self):
if self.options['font_size'] < 1:
return None
fontsize = int(self.options['font_size'])
fontname = self.options['font_name_r']
try:
id = '%s.%s' % (text_type(fontname), text_type(fontsize))
except UnicodeDecodeError:
id = '%s.%s' % (fontname, fontsize)
if id not in self._cache:
font = ImageFont.truetype(fontname, fontsize)
self._cache[id] = font
return self._cache[id]
def get_extents(self, text):
font = self._select_font()
if not font:
return 0, 0
left, top, right, bottom = font.getbbox(text)
ascent, descent = font.getmetrics()
if self.options['limit_render_to_text_bbox']:
h = bottom - top
else:
h = ascent + descent
w = right - left
return w, h
def get_cached_extents(self):
return self.get_extents
def _render_begin(self):
# create a surface, context, font...
self._pil_im = Image.new('RGBA', self._size, color=(255, 255, 255, 0))
self._pil_draw = ImageDraw.Draw(self._pil_im)
def _render_text(self, text, x, y):
font = self._select_font()
if not font:
return
color = tuple([int(c * 255) for c in self.options['color']])
# Adjust x and y position to avoid text cutoff
if self.options['limit_render_to_text_bbox']:
bbox = font.getbbox(text)
x -= bbox[0]
y -= bbox[1]
self._pil_draw.text((int(x), int(y)), text, font=font, fill=color)
def _render_end(self):
data = ImageData(self._size[0], self._size[1],
self._pil_im.mode.lower(), self._pil_im.tobytes())
del self._pil_im
del self._pil_draw
return data
@@ -0,0 +1,117 @@
'''
Text Pygame: Draw text with pygame
.. warning::
Pygame has been deprecated and will be removed in the release after Kivy
1.11.0.
'''
__all__ = ('LabelPygame', )
from kivy.compat import PY2
from kivy.core.text import LabelBase
from kivy.core.image import ImageData
from kivy.utils import deprecated
try:
import pygame
except:
raise
pygame_cache = {}
pygame_font_handles = {}
pygame_cache_order = []
# init pygame font
try:
pygame.ftfont.init()
except:
pygame.font.init()
class LabelPygame(LabelBase):
@deprecated(
msg='Pygame has been deprecated and will be removed after 1.11.0')
def __init__(self, *largs, **kwargs):
super(LabelPygame, self).__init__(*largs, **kwargs)
def _get_font_id(self):
return '|'.join([str(self.options[x]) for x in
('font_size', 'font_name_r', 'bold', 'italic')])
def _get_font(self):
fontid = self._get_font_id()
if fontid not in pygame_cache:
# try first the file if it's a filename
font_handle = fontobject = None
fontname = self.options['font_name_r']
ext = fontname.rsplit('.', 1)
if len(ext) == 2:
# try to open the font if it has an extension
font_handle = open(fontname, 'rb')
fontobject = pygame.font.Font(font_handle,
int(self.options['font_size']))
# fallback to search a system font
if fontobject is None:
# try to search the font
font = pygame.font.match_font(
self.options['font_name_r'].replace(' ', ''),
bold=self.options['bold'],
italic=self.options['italic'])
# fontobject
fontobject = pygame.font.Font(font,
int(self.options['font_size']))
pygame_cache[fontid] = fontobject
pygame_font_handles[fontid] = font_handle
pygame_cache_order.append(fontid)
# to prevent too much file open, limit the number of opened fonts to 64
while len(pygame_cache_order) > 64:
popid = pygame_cache_order.pop(0)
del pygame_cache[popid]
font_handle = pygame_font_handles.pop(popid)
if font_handle is not None:
font_handle.close()
return pygame_cache[fontid]
def get_ascent(self):
return self._get_font().get_ascent()
def get_descent(self):
return self._get_font().get_descent()
def get_extents(self, text):
return self._get_font().size(text)
def get_cached_extents(self):
return self._get_font().size
def _render_begin(self):
self._pygame_surface = pygame.Surface(self._size, pygame.SRCALPHA, 32)
self._pygame_surface.fill((0, 0, 0, 0))
def _render_text(self, text, x, y):
font = self._get_font()
color = [c * 255 for c in self.options['color']]
color[0], color[2] = color[2], color[0]
try:
text = font.render(text, True, color)
text.set_colorkey(color)
self._pygame_surface.blit(text, (x, y), None,
pygame.BLEND_RGBA_ADD)
except pygame.error:
pass
def _render_end(self):
w, h = self._size
data = ImageData(w, h,
'rgba', self._pygame_surface.get_buffer().raw)
del self._pygame_surface
return data
@@ -0,0 +1,50 @@
'''
SDL2 text provider
==================
Based on SDL2 + SDL2_ttf
'''
__all__ = ('LabelSDL2', )
from kivy.compat import PY2
from kivy.core.text import LabelBase
try:
from kivy.core.text._text_sdl2 import (_SurfaceContainer, _get_extents,
_get_fontdescent, _get_fontascent)
except ImportError:
from kivy.core import handle_win_lib_import_error
handle_win_lib_import_error(
'text', 'sdl2', 'kivy.core.text._text_sdl2')
raise
class LabelSDL2(LabelBase):
def _get_font_id(self):
return '|'.join([str(self.options[x]) for x
in ('font_size', 'font_name_r', 'bold',
'italic', 'underline', 'strikethrough')])
def get_extents(self, text):
try:
if PY2:
text = text.encode('UTF-8')
except:
pass
return _get_extents(self, text)
def get_descent(self):
return _get_fontdescent(self)
def get_ascent(self):
return _get_fontascent(self)
def _render_begin(self):
self._surface = _SurfaceContainer(self._size[0], self._size[1])
def _render_text(self, text, x, y):
self._surface.render(self, text, x, y)
def _render_end(self):
return self._surface.get_data()
@@ -0,0 +1,219 @@
'''
Video
=====
Core class for reading video files and managing the video
:class:`~kivy.graphics.texture.Texture`.
.. versionchanged:: 1.10.0
The pyglet, pygst and gi providers have been removed.
.. versionchanged:: 1.8.0
There are now 2 distinct Gstreamer implementations: one using Gi/Gst
working for both Python 2+3 with Gstreamer 1.0, and one using PyGST
working only for Python 2 + Gstreamer 0.10.
.. note::
Recording is not supported.
'''
__all__ = ('VideoBase', 'Video')
from kivy.clock import Clock
from kivy.core import core_select_lib
from kivy.event import EventDispatcher
from kivy.logger import Logger
from kivy.compat import PY2
class VideoBase(EventDispatcher):
'''VideoBase, a class used to implement a video reader.
:Parameters:
`filename`: str
Filename of the video. Can be a file or an URI.
`eos`: str, defaults to 'pause'
Action to take when EOS is hit. Can be one of 'pause', 'stop' or
'loop'.
.. versionchanged:: 1.4.0
added 'pause'
`async`: bool, defaults to True
Load the video asynchronously (may be not supported by all
providers).
`autoplay`: bool, defaults to False
Auto play the video on init.
:Events:
`on_eos`
Fired when EOS is hit.
`on_load`
Fired when the video is loaded and the texture is available.
`on_frame`
Fired when a new frame is written to the texture.
'''
__slots__ = ('_wantplay', '_buffer', '_filename', '_texture',
'_volume', 'eos', '_state', '_async', '_autoplay')
__events__ = ('on_eos', 'on_load', 'on_frame')
def __init__(self, **kwargs):
kwargs.setdefault('filename', None)
kwargs.setdefault('eos', 'stop')
kwargs.setdefault('async', True)
kwargs.setdefault('autoplay', False)
super(VideoBase, self).__init__()
self._wantplay = False
self._buffer = None
self._filename = None
self._texture = None
self._volume = 1.
self._state = ''
self._autoplay = kwargs.get('autoplay')
self._async = kwargs.get('async')
self.eos = kwargs.get('eos')
if self.eos == 'pause':
Logger.warning("'pause' is deprecated. Use 'stop' instead.")
self.eos = 'stop'
self.filename = kwargs.get('filename')
Clock.schedule_interval(self._update, 1 / 30.)
if self._autoplay:
self.play()
def __del__(self):
self.unload()
def on_eos(self):
pass
def on_load(self):
pass
def on_frame(self):
pass
def _get_filename(self):
return self._filename
def _set_filename(self, filename):
if filename == self._filename:
return
self.unload()
self._filename = filename
if self._filename is None:
return
self.load()
filename = property(lambda self: self._get_filename(),
lambda self, x: self._set_filename(x),
doc='Get/set the filename/uri of the current video')
def _get_position(self):
return 0
def _set_position(self, pos):
self.seek(pos)
position = property(lambda self: self._get_position(),
lambda self, x: self._set_position(x),
doc='Get/set the position in the video (in seconds)')
def _get_volume(self):
return self._volume
def _set_volume(self, volume):
self._volume = volume
volume = property(lambda self: self._get_volume(),
lambda self, x: self._set_volume(x),
doc='Get/set the volume in the video (1.0 = 100%)')
def _get_duration(self):
return 0
duration = property(lambda self: self._get_duration(),
doc='Get the video duration (in seconds)')
def _get_texture(self):
return self._texture
texture = property(lambda self: self._get_texture(),
doc='Get the video texture')
def _get_state(self):
return self._state
state = property(lambda self: self._get_state(),
doc='Get the video playing status')
def _do_eos(self, *args):
'''
.. versionchanged:: 1.4.0
Now dispatches the `on_eos` event.
'''
if self.eos == 'pause':
self.pause()
elif self.eos == 'stop':
self.stop()
elif self.eos == 'loop':
self.position = 0
self.play()
self.dispatch('on_eos')
def _update(self, dt):
'''Update the video content to texture.
'''
pass
def seek(self, percent, precise=True):
'''Move to position as percentage (strictly, a proportion from
0 - 1) of the duration'''
pass
def stop(self):
'''Stop the video playing'''
self._state = ''
def pause(self):
'''Pause the video
.. versionadded:: 1.4.0
'''
self._state = 'paused'
def play(self):
'''Play the video'''
self._state = 'playing'
def load(self):
'''Load the video from the current filename'''
pass
def unload(self):
'''Unload the actual video'''
self._state = ''
# Load the appropriate provider
video_providers = []
try:
from kivy.lib.gstplayer import GstPlayer # NOQA
video_providers += [('gstplayer', 'video_gstplayer', 'VideoGstplayer')]
except ImportError:
pass
video_providers += [
('ffmpeg', 'video_ffmpeg', 'VideoFFMpeg'),
('ffpyplayer', 'video_ffpyplayer', 'VideoFFPy'),
('null', 'video_null', 'VideoNull')]
Video = core_select_lib('video', video_providers)
@@ -0,0 +1,106 @@
'''
FFmpeg video abstraction
========================
.. versionadded:: 1.0.8
This abstraction requires ffmpeg python extensions. We have made a special
extension that is used for the android platform but can also be used on x86
platforms. The project is available at::
http://github.com/tito/ffmpeg-android
The extension is designed for implementing a video player.
Refer to the documentation of the ffmpeg-android project for more information
about the requirements.
'''
try:
import ffmpeg
except:
raise
from kivy.core.video import VideoBase
from kivy.graphics.texture import Texture
class VideoFFMpeg(VideoBase):
def __init__(self, **kwargs):
self._do_load = False
self._player = None
super(VideoFFMpeg, self).__init__(**kwargs)
def unload(self):
if self._player:
self._player.stop()
self._player = None
self._state = ''
self._do_load = False
def load(self):
self.unload()
def play(self):
if self._player:
self.unload()
self._player = ffmpeg.FFVideo(self._filename)
self._player.set_volume(self._volume)
self._do_load = True
def stop(self):
self.unload()
def seek(self, percent, precise=True):
if self._player is None:
return
self._player.seek(percent)
def _do_eos(self):
self.unload()
self.dispatch('on_eos')
super(VideoFFMpeg, self)._do_eos()
def _update(self, dt):
if self._do_load:
self._player.open()
self._do_load = False
return
player = self._player
if player is None:
return
if not player.is_open:
self._do_eos()
return
frame = player.get_next_frame()
if frame is None:
return
# first time we got a frame, we know that video is read now.
if self._texture is None:
self._texture = Texture.create(size=(
player.get_width(), player.get_height()),
colorfmt='rgb')
self._texture.flip_vertical()
self.dispatch('on_load')
if self._texture:
self._texture.blit_buffer(frame)
self.dispatch('on_frame')
def _get_duration(self):
if self._player is None:
return 0
return self._player.get_duration()
def _get_position(self):
if self._player is None:
return 0
return self._player.get_position()
def _set_volume(self, value):
self._volume = value
if self._player:
self._player.set_volume(self._volume)
@@ -0,0 +1,449 @@
'''
FFmpeg based video abstraction
==============================
To use, you need to install ffpyplayer and have a compiled ffmpeg shared
library.
https://github.com/matham/ffpyplayer
The docs there describe how to set this up. But briefly, first you need to
compile ffmpeg using the shared flags while disabling the static flags (you'll
probably have to set the fPIC flag, e.g. CFLAGS=-fPIC). Here are some
instructions: https://trac.ffmpeg.org/wiki/CompilationGuide. For Windows, you
can download compiled GPL binaries from http://ffmpeg.zeranoe.com/builds/.
Similarly, you should download SDL2.
Now, you should have ffmpeg and sdl directories. In each, you should have an
'include', 'bin' and 'lib' directory, where e.g. for Windows, 'lib' contains
the .dll.a files, while 'bin' contains the actual dlls. The 'include' directory
holds the headers. The 'bin' directory is only needed if the shared libraries
are not already in the path. In the environment, define FFMPEG_ROOT and
SDL_ROOT, each pointing to the ffmpeg and SDL directories respectively. (If
you're using SDL2, the 'include' directory will contain an 'SDL2' directory,
which then holds the headers).
Once defined, download the ffpyplayer git repo and run
python setup.py build_ext --inplace
Finally, before running you need to ensure that ffpyplayer is in python's path.
..Note::
When kivy exits by closing the window while the video is playing,
it appears that the __del__method of VideoFFPy
is not called. Because of this, the VideoFFPy object is not
properly deleted when kivy exits. The consequence is that because
MediaPlayer creates internal threads which do not have their daemon
flag set, when the main threads exists, it'll hang and wait for the other
MediaPlayer threads to exit. But since __del__ is not called to delete the
MediaPlayer object, those threads will remain alive, hanging kivy. What
this means is that you have to be sure to delete the MediaPlayer object
before kivy exits by setting it to None.
'''
__all__ = ('VideoFFPy', )
try:
import ffpyplayer
from ffpyplayer.player import MediaPlayer
from ffpyplayer.tools import set_log_callback, get_log_callback
except:
raise
from threading import Thread
from queue import Queue, Empty, Full
from kivy.clock import Clock, mainthread
from kivy.logger import Logger
from kivy.core.video import VideoBase
from kivy.graphics import Rectangle, BindTexture
from kivy.graphics.texture import Texture
from kivy.graphics.fbo import Fbo
from kivy.weakmethod import WeakMethod
import time
Logger.info('VideoFFPy: Using ffpyplayer {}'.format(ffpyplayer.version))
logger_func = {'quiet': Logger.critical, 'panic': Logger.critical,
'fatal': Logger.critical, 'error': Logger.error,
'warning': Logger.warning, 'info': Logger.info,
'verbose': Logger.debug, 'debug': Logger.debug}
def _log_callback(message, level):
message = message.strip()
if message:
logger_func[level]('ffpyplayer: {}'.format(message))
if not get_log_callback():
set_log_callback(_log_callback)
class VideoFFPy(VideoBase):
YUV_RGB_FS = """
$HEADER$
uniform sampler2D tex_y;
uniform sampler2D tex_u;
uniform sampler2D tex_v;
void main(void) {
float y = texture2D(tex_y, tex_coord0).r;
float u = texture2D(tex_u, tex_coord0).r - 0.5;
float v = texture2D(tex_v, tex_coord0).r - 0.5;
float r = y + 1.402 * v;
float g = y - 0.344 * u - 0.714 * v;
float b = y + 1.772 * u;
gl_FragColor = vec4(r, g, b, 1.0);
}
"""
_trigger = None
def __init__(self, **kwargs):
self._ffplayer = None
self._thread = None
self._next_frame = None
self._seek_queue = []
self._ffplayer_need_quit = False
self._wakeup_queue = Queue(maxsize=1)
self._trigger = Clock.create_trigger(self._redraw)
super(VideoFFPy, self).__init__(**kwargs)
@property
def _is_stream(self):
# This is only used when building ff_opts, to prevent starting
# player paused and can probably be removed as soon as the 'eof'
# receiving issue is solved.
# See https://github.com/matham/ffpyplayer/issues/142
return self.filename.startswith('rtsp://')
def __del__(self):
self.unload()
def _wakeup_thread(self):
try:
self._wakeup_queue.put(None, False)
except Full:
pass
def _wait_for_wakeup(self, timeout):
try:
self._wakeup_queue.get(True, timeout)
except Empty:
pass
def _player_callback(self, selector, value):
if self._ffplayer is None:
return
if selector == 'quit':
def close(*args):
self.unload()
Clock.schedule_once(close, 0)
def _get_position(self):
if self._ffplayer is not None:
return self._ffplayer.get_pts()
return 0
def _set_position(self, pos):
self.seek(pos)
def _set_volume(self, volume):
self._volume = volume
if self._ffplayer is not None:
self._ffplayer.set_volume(self._volume)
def _get_duration(self):
if self._ffplayer is None:
return 0
return self._ffplayer.get_metadata()['duration']
@mainthread
def _do_eos(self):
if self.eos == 'pause':
self.pause()
elif self.eos == 'stop':
self.stop()
elif self.eos == 'loop':
# this causes a seek to zero
self.position = 0
self.dispatch('on_eos')
@mainthread
def _finish_setup(self):
# once setup is done, we make sure player state matches what user
# could have requested while player was being setup and it was in limbo
# also, thread starts player in internal paused mode, so this unpauses
# it if user didn't request pause meanwhile
if self._ffplayer is not None:
self._ffplayer.set_volume(self._volume)
self._ffplayer.set_pause(self._state == 'paused')
self._wakeup_thread()
def _redraw(self, *args):
if not self._ffplayer:
return
next_frame = self._next_frame
if not next_frame:
return
img, pts = next_frame
if img.get_size() != self._size or self._texture is None:
self._size = w, h = img.get_size()
if self._out_fmt == 'yuv420p':
w2 = int(w / 2)
h2 = int(h / 2)
self._tex_y = Texture.create(
size=(w, h), colorfmt='luminance')
self._tex_u = Texture.create(
size=(w2, h2), colorfmt='luminance')
self._tex_v = Texture.create(
size=(w2, h2), colorfmt='luminance')
self._fbo = fbo = Fbo(size=self._size)
with fbo:
BindTexture(texture=self._tex_u, index=1)
BindTexture(texture=self._tex_v, index=2)
Rectangle(size=fbo.size, texture=self._tex_y)
fbo.shader.fs = VideoFFPy.YUV_RGB_FS
fbo['tex_y'] = 0
fbo['tex_u'] = 1
fbo['tex_v'] = 2
self._texture = fbo.texture
else:
self._texture = Texture.create(size=self._size,
colorfmt='rgba')
# XXX FIXME
# self.texture.add_reload_observer(self.reload_buffer)
self._texture.flip_vertical()
self.dispatch('on_load')
if self._texture:
if self._out_fmt == 'yuv420p':
dy, du, dv, _ = img.to_memoryview()
if dy and du and dv:
self._tex_y.blit_buffer(dy, colorfmt='luminance')
self._tex_u.blit_buffer(du, colorfmt='luminance')
self._tex_v.blit_buffer(dv, colorfmt='luminance')
self._fbo.ask_update()
self._fbo.draw()
else:
self._texture.blit_buffer(
img.to_memoryview()[0], colorfmt='rgba')
self.dispatch('on_frame')
def _next_frame_run(self, ffplayer):
sleep = time.sleep
trigger = self._trigger
did_dispatch_eof = False
wait_for_wakeup = self._wait_for_wakeup
seek_queue = self._seek_queue
# video starts in internal paused state
# fast path, if the source video is yuv420p, we'll use a glsl shader
# for buffer conversion to rgba
# wait until we get frame metadata
while not self._ffplayer_need_quit:
src_pix_fmt = ffplayer.get_metadata().get('src_pix_fmt')
if not src_pix_fmt:
wait_for_wakeup(0.005)
continue
# ffpyplayer reports src_pix_fmt as bytes. this may or may not
# change in future, so we check for both bytes and str
if src_pix_fmt in (b'yuv420p', 'yuv420p'):
self._out_fmt = 'yuv420p'
ffplayer.set_output_pix_fmt(self._out_fmt)
break
if self._ffplayer_need_quit:
ffplayer.close_player()
return
self._ffplayer = ffplayer
self._finish_setup()
# now, we'll be in internal paused state and loop will wait until
# mainthread unpauses us when finishing setup
while not self._ffplayer_need_quit:
seek_happened = False
if seek_queue:
vals = seek_queue[:]
del seek_queue[:len(vals)]
percent, precise = vals[-1]
ffplayer.seek(
percent * ffplayer.get_metadata()['duration'],
relative=False,
accurate=precise
)
seek_happened = True
did_dispatch_eof = False
self._next_frame = None
# Get next frame if paused:
if seek_happened and ffplayer.get_pause():
ffplayer.set_volume(0.0) # Try to do it silently.
ffplayer.set_pause(False)
try:
# We don't know concrete number of frames to skip,
# this number worked fine on couple of tested videos:
to_skip = 6
while True:
frame, val = ffplayer.get_frame(show=False)
# Exit loop on invalid val:
if val in ('paused', 'eof'):
break
# Exit loop on seek_queue updated:
if seek_queue:
break
# Wait for next frame:
if frame is None:
sleep(0.005)
continue
# Wait until we skipped enough frames:
to_skip -= 1
if to_skip == 0:
break
# Assuming last frame is actual, just get it:
frame, val = ffplayer.get_frame(force_refresh=True)
finally:
ffplayer.set_pause(bool(self._state == 'paused'))
# todo: this is not safe because user could have updated
# volume between us reading it and setting it
ffplayer.set_volume(self._volume)
# Get next frame regular:
else:
frame, val = ffplayer.get_frame()
if val == 'eof':
if not did_dispatch_eof:
self._do_eos()
did_dispatch_eof = True
wait_for_wakeup(None)
elif val == 'paused':
did_dispatch_eof = False
wait_for_wakeup(None)
else:
did_dispatch_eof = False
if frame:
self._next_frame = frame
trigger()
else:
val = val if val else (1 / 30.)
wait_for_wakeup(val)
ffplayer.close_player()
def seek(self, percent, precise=True):
# still save seek while thread is setting up
self._seek_queue.append((percent, precise,))
self._wakeup_thread()
def stop(self):
self.unload()
def pause(self):
# if state hasn't been set (empty), there's no player. If it's
# paused, nothing to do so just handle playing
if self._state == 'playing':
# we could be in limbo while player is setting up so check. Player
# will pause when finishing setting up
if self._ffplayer is not None:
self._ffplayer.set_pause(True)
# even in limbo, indicate to start in paused state
self._state = 'paused'
self._wakeup_thread()
def play(self):
# _state starts empty and is empty again after unloading
if self._ffplayer:
# player is already setup, just handle unpausing
assert self._state in ('paused', 'playing')
if self._state == 'paused':
self._ffplayer.set_pause(False)
self._state = 'playing'
self._wakeup_thread()
return
# we're now either in limbo state waiting for thread to setup,
# or no thread has been started
if self._state == 'playing':
# in limbo, just wait for thread to setup player
return
elif self._state == 'paused':
# in limbo, still unpause for when player becomes ready
self._state = 'playing'
self._wakeup_thread()
return
# load first unloads
self.load()
self._out_fmt = 'rgba'
# if no stream, it starts internally paused, but unpauses itself
# if stream and we start paused, we sometimes receive eof after a
# few frames, depending on the stream producer.
# XXX: This probably needs to be figured out in ffpyplayer, using
# ffplay directly works.
ff_opts = {
'paused': not self._is_stream,
'out_fmt': self._out_fmt,
'sn': True,
'volume': self._volume,
}
ffplayer = MediaPlayer(
self._filename, callback=self._player_callback,
thread_lib='SDL',
loglevel='info', ff_opts=ff_opts
)
# Disabled as an attempt to fix kivy issue #6210
# self._ffplayer.set_volume(self._volume)
self._thread = Thread(
target=self._next_frame_run,
name='Next frame',
args=(ffplayer, )
)
# todo: remove
self._thread.daemon = True
# start in playing mode, but _ffplayer isn't set until ready. We're
# now in a limbo state
self._state = 'playing'
self._thread.start()
def load(self):
self.unload()
def unload(self):
# no need to call self._trigger.cancel() because _ffplayer is set
# to None below, and it's not safe to call clock stuff from __del__
# if thread is still alive, set it to exit and wake it
self._wakeup_thread()
self._ffplayer_need_quit = True
# wait until it exits
if self._thread:
# TODO: use callback, don't block here
self._thread.join()
self._thread = None
if self._ffplayer:
self._ffplayer = None
self._next_frame = None
self._size = (0, 0)
self._state = ''
self._seek_queue = []
# reset for next load since thread is dead for sure
self._ffplayer_need_quit = False
self._wakeup_queue = Queue(maxsize=1)
@@ -0,0 +1,140 @@
'''
Video Gstplayer
===============
.. versionadded:: 1.8.0
Implementation of a VideoBase with Kivy :class:`~kivy.lib.gstplayer.GstPlayer`
This player is the preferred player, using Gstreamer 1.0, working on both
Python 2 and 3.
'''
try:
from kivy.lib.gstplayer import GstPlayer, get_gst_version
except ImportError:
from kivy.core import handle_win_lib_import_error
handle_win_lib_import_error(
'VideoGstplayer', 'gst', 'kivy.lib.gstplayer._gstplayer')
raise
from kivy.graphics.texture import Texture
from kivy.core.video import VideoBase
from kivy.logger import Logger
from kivy.clock import Clock
from kivy.compat import PY2
from threading import Lock
from functools import partial
from os.path import realpath
from weakref import ref
if PY2:
from urllib import pathname2url
else:
from urllib.request import pathname2url
Logger.info('VideoGstplayer: Using Gstreamer {}'.format(
'.'.join(map(str, get_gst_version()))))
def _on_gstplayer_buffer(video, width, height, data):
video = video()
# if we still receive the video but no more player, remove it.
if not video:
return
with video._buffer_lock:
video._buffer = (width, height, data)
def _on_gstplayer_message(mtype, message):
if mtype == 'error':
Logger.error('VideoGstplayer: {}'.format(message))
elif mtype == 'warning':
Logger.warning('VideoGstplayer: {}'.format(message))
elif mtype == 'info':
Logger.info('VideoGstplayer: {}'.format(message))
class VideoGstplayer(VideoBase):
def __init__(self, **kwargs):
self.player = None
self._buffer = None
self._buffer_lock = Lock()
super(VideoGstplayer, self).__init__(**kwargs)
def _on_gst_eos_sync(self):
Clock.schedule_once(self._do_eos, 0)
def load(self):
Logger.debug('VideoGstplayer: Load <{}>'.format(self._filename))
uri = self._get_uri()
wk_self = ref(self)
self.player_callback = partial(_on_gstplayer_buffer, wk_self)
self.player = GstPlayer(uri, self.player_callback,
self._on_gst_eos_sync, _on_gstplayer_message)
self.player.load()
def unload(self):
if self.player:
self.player.unload()
self.player = None
with self._buffer_lock:
self._buffer = None
self._texture = None
def stop(self):
super(VideoGstplayer, self).stop()
self.player.stop()
def pause(self):
super(VideoGstplayer, self).pause()
self.player.pause()
def play(self):
super(VideoGstplayer, self).play()
self.player.set_volume(self.volume)
self.player.play()
def seek(self, percent, precise=True):
self.player.seek(percent)
def _get_position(self):
return self.player.get_position()
def _get_duration(self):
return self.player.get_duration()
def _set_volume(self, value):
self._volume = value
if self.player:
self.player.set_volume(self._volume)
def _update(self, dt):
buf = None
with self._buffer_lock:
buf = self._buffer
self._buffer = None
if buf is not None:
self._update_texture(buf)
self.dispatch('on_frame')
def _update_texture(self, buf):
width, height, data = buf
# texture is not allocated yet, create it first
if not self._texture:
self._texture = Texture.create(size=(width, height),
colorfmt='rgb')
self._texture.flip_vertical()
self.dispatch('on_load')
if self._texture:
self._texture.blit_buffer(
data, size=(width, height), colorfmt='rgb')
def _get_uri(self):
uri = self.filename
if not uri:
return
if '://' not in uri:
uri = 'file:' + pathname2url(realpath(uri))
return uri
@@ -0,0 +1,12 @@
'''
VideoNull: empty implementation of VideoBase for the no provider case
'''
from kivy.core.video import VideoBase
class VideoNull(VideoBase):
'''VideoBase implementation when there is no provider.
'''
pass
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
include "../../include/config.pxi"
IF USE_WAYLAND:
cdef extern from "wayland-client-protocol.h":
cdef struct wl_display:
pass
cdef struct wl_surface:
pass
cdef struct wl_shell_surface:
pass
IF USE_X11:
cdef extern from "X11/Xlib.h":
cdef struct _XDisplay:
pass
ctypedef _XDisplay Display
ctypedef int XID
ctypedef XID Window
IF UNAME_SYSNAME == 'Windows':
cdef extern from "windows.h":
ctypedef void *HANDLE
ctypedef HANDLE HWND
ctypedef HANDLE HDC
ctypedef HANDLE HINSTANCE
@@ -0,0 +1,86 @@
'''
EGL Rpi Window: EGL Window provider, specialized for the Pi
Inspired by: rpi_vid_core + JF002 rpi kivy repo
'''
__all__ = ('WindowEglRpi', )
from kivy.logger import Logger
from kivy.core.window import WindowBase
from kivy.base import EventLoop, ExceptionManager, stopTouchApp
from kivy.lib.vidcore_lite import bcm, egl
from os import environ
# Default display IDs.
(DISPMANX_ID_MAIN_LCD,
DISPMANX_ID_AUX_LCD,
DISPMANX_ID_HDMI,
DISPMANX_ID_SDTV,
DISPMANX_ID_FORCE_LCD,
DISPMANX_ID_FORCE_TV,
DISPMANX_ID_FORCE_OTHER) = range(7)
class WindowEglRpi(WindowBase):
_rpi_dispmanx_id = int(environ.get("KIVY_BCM_DISPMANX_ID", "0"))
_rpi_dispmanx_layer = int(environ.get("KIVY_BCM_DISPMANX_LAYER", "0"))
gl_backends_ignored = ['sdl2']
def create_window(self):
bcm.host_init()
w, h = bcm.graphics_get_display_size(self._rpi_dispmanx_id)
Logger.debug('Window: Actual display size: {}x{}'.format(
w, h))
self._size = w, h
self._create_window(w, h)
self._create_egl_context(self.win, 0)
super(WindowEglRpi, self).create_window()
def _create_window(self, w, h):
dst = bcm.Rect(0, 0, w, h)
src = bcm.Rect(0, 0, w << 16, h << 16)
display = egl.bcm_display_open(self._rpi_dispmanx_id)
update = egl.bcm_update_start(0)
element = egl.bcm_element_add(
update, display, self._rpi_dispmanx_layer, dst, src)
self.win = egl.NativeWindow(element, w, h)
egl.bcm_update_submit_sync(update)
def _create_egl_context(self, win, flags):
api = egl._constants.EGL_OPENGL_ES_API
c = egl._constants
attribs = [
c.EGL_RED_SIZE, 8,
c.EGL_GREEN_SIZE, 8,
c.EGL_BLUE_SIZE, 8,
c.EGL_ALPHA_SIZE, 8,
c.EGL_DEPTH_SIZE, 16,
c.EGL_STENCIL_SIZE, 8,
c.EGL_SURFACE_TYPE, c.EGL_WINDOW_BIT,
c.EGL_NONE]
attribs_context = [c.EGL_CONTEXT_CLIENT_VERSION, 2, c.EGL_NONE]
display = egl.GetDisplay(c.EGL_DEFAULT_DISPLAY)
egl.Initialise(display)
egl.BindAPI(c.EGL_OPENGL_ES_API)
egl.GetConfigs(display)
config = egl.ChooseConfig(display, attribs, 1)[0]
surface = egl.CreateWindowSurface(display, config, win)
context = egl.CreateContext(display, config, None, attribs_context)
egl.MakeCurrent(display, surface, surface, context)
self.egl_info = (display, surface, context)
egl.MakeCurrent(display, surface, surface, context)
def close(self):
egl.Terminate(self.egl_info[0])
def flip(self):
if not EventLoop.quit:
egl.SwapBuffers(self.egl_info[0], self.egl_info[1])
@@ -0,0 +1,19 @@
include "window_attrs.pxi"
from libc.stdint cimport uintptr_t
IF USE_WAYLAND:
cdef class WindowInfoWayland:
cdef wl_display *display
cdef wl_surface *surface
cdef wl_shell_surface *shell_surface
IF USE_X11:
cdef class WindowInfoX11:
cdef Display *display
cdef Window window
IF UNAME_SYSNAME == 'Windows':
cdef class WindowInfoWindows:
cdef HWND window
cdef HDC hdc
@@ -0,0 +1,441 @@
'''
Window Pygame: windowing provider based on Pygame
.. warning::
Pygame has been deprecated and will be removed in the release after Kivy
1.11.0.
'''
__all__ = ('WindowPygame', )
# fail early if possible
import pygame
from kivy.compat import PY2
from kivy.core.window import WindowBase
from kivy.core import CoreCriticalException
from os import environ
from os.path import exists, join
from kivy.config import Config
from kivy import kivy_data_dir
from kivy.base import ExceptionManager
from kivy.logger import Logger
from kivy.base import stopTouchApp, EventLoop
from kivy.utils import platform, deprecated
from kivy.resources import resource_find
try:
android = None
if platform == 'android':
import android
except ImportError:
pass
# late binding
glReadPixels = GL_RGBA = GL_UNSIGNED_BYTE = None
class WindowPygame(WindowBase):
@deprecated(
msg='Pygame has been deprecated and will be removed after 1.11.0')
def __init__(self, *largs, **kwargs):
super(WindowPygame, self).__init__(*largs, **kwargs)
def create_window(self, *largs):
# ensure the mouse is still not up after window creation, otherwise, we
# have some weird bugs
self.dispatch('on_mouse_up', 0, 0, 'all', [])
# force display to show (available only for fullscreen)
displayidx = Config.getint('graphics', 'display')
if 'SDL_VIDEO_FULLSCREEN_HEAD' not in environ and displayidx != -1:
environ['SDL_VIDEO_FULLSCREEN_HEAD'] = '%d' % displayidx
# init some opengl, same as before.
self.flags = pygame.HWSURFACE | pygame.OPENGL | pygame.DOUBLEBUF
# right now, activate resizable window only on linux.
# on window / macosx, the opengl context is lost, and we need to
# reconstruct everything. Check #168 for a state of the work.
if platform in ('linux', 'macosx', 'win') and \
Config.getboolean('graphics', 'resizable'):
self.flags |= pygame.RESIZABLE
try:
pygame.display.init()
except pygame.error as e:
raise CoreCriticalException(e.message)
multisamples = Config.getint('graphics', 'multisamples')
if multisamples > 0:
pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLEBUFFERS, 1)
pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLESAMPLES,
multisamples)
pygame.display.gl_set_attribute(pygame.GL_DEPTH_SIZE, 16)
pygame.display.gl_set_attribute(pygame.GL_STENCIL_SIZE, 1)
pygame.display.set_caption(self.title)
if self.position == 'auto':
self._pos = None
elif self.position == 'custom':
self._pos = self.left, self.top
else:
raise ValueError('position token in configuration accept only '
'"auto" or "custom"')
if self._fake_fullscreen:
if not self.borderless:
self.fullscreen = self._fake_fullscreen = False
elif not self.fullscreen or self.fullscreen == 'auto':
self.borderless = self._fake_fullscreen = False
if self.fullscreen == 'fake':
self.borderless = self._fake_fullscreen = True
Logger.warning("The 'fake' fullscreen option has been "
"deprecated, use Window.borderless or the "
"borderless Config option instead.")
if self.fullscreen == 'fake' or self.borderless:
Logger.debug('WinPygame: Set window to borderless mode.')
self.flags |= pygame.NOFRAME
# If no position set in borderless mode, we always need
# to set the position. So use 0, 0.
if self._pos is None:
self._pos = (0, 0)
environ['SDL_VIDEO_WINDOW_POS'] = '%d,%d' % self._pos
elif self.fullscreen in ('auto', True):
Logger.debug('WinPygame: Set window to fullscreen mode')
self.flags |= pygame.FULLSCREEN
elif self._pos is not None:
environ['SDL_VIDEO_WINDOW_POS'] = '%d,%d' % self._pos
# never stay with a None pos, application using w.center will be fired.
self._pos = (0, 0)
# prepare keyboard
repeat_delay = int(Config.get('kivy', 'keyboard_repeat_delay'))
repeat_rate = float(Config.get('kivy', 'keyboard_repeat_rate'))
pygame.key.set_repeat(repeat_delay, int(1000. / repeat_rate))
# set window icon before calling set_mode
try:
filename_icon = self.icon or Config.get('kivy', 'window_icon')
if filename_icon == '':
logo_size = 32
if platform == 'macosx':
logo_size = 512
elif platform == 'win':
logo_size = 64
filename_icon = 'kivy-icon-{}.png'.format(logo_size)
filename_icon = resource_find(
join(kivy_data_dir, 'logo', filename_icon))
self.set_icon(filename_icon)
except:
Logger.exception('Window: cannot set icon')
# try to use mode with multisamples
try:
self._pygame_set_mode()
except pygame.error as e:
if multisamples:
Logger.warning('WinPygame: Video: failed (multisamples=%d)' %
multisamples)
Logger.warning('WinPygame: trying without antialiasing')
pygame.display.gl_set_attribute(
pygame.GL_MULTISAMPLEBUFFERS, 0)
pygame.display.gl_set_attribute(
pygame.GL_MULTISAMPLESAMPLES, 0)
multisamples = 0
try:
self._pygame_set_mode()
except pygame.error as e:
raise CoreCriticalException(e.message)
else:
raise CoreCriticalException(e.message)
if pygame.RESIZABLE & self.flags:
self._pygame_set_mode()
info = pygame.display.Info()
self._size = (info.current_w, info.current_h)
# self.dispatch('on_resize', *self._size)
# in order to debug futur issue with pygame/display, let's show
# more debug output.
Logger.debug('Window: Display driver ' + pygame.display.get_driver())
Logger.debug('Window: Actual window size: %dx%d',
info.current_w, info.current_h)
if platform != 'android':
# unsupported platform, such as android that doesn't support
# gl_get_attribute.
Logger.debug(
'Window: Actual color bits r%d g%d b%d a%d',
pygame.display.gl_get_attribute(pygame.GL_RED_SIZE),
pygame.display.gl_get_attribute(pygame.GL_GREEN_SIZE),
pygame.display.gl_get_attribute(pygame.GL_BLUE_SIZE),
pygame.display.gl_get_attribute(pygame.GL_ALPHA_SIZE))
Logger.debug(
'Window: Actual depth bits: %d',
pygame.display.gl_get_attribute(pygame.GL_DEPTH_SIZE))
Logger.debug(
'Window: Actual stencil bits: %d',
pygame.display.gl_get_attribute(pygame.GL_STENCIL_SIZE))
Logger.debug(
'Window: Actual multisampling samples: %d',
pygame.display.gl_get_attribute(pygame.GL_MULTISAMPLESAMPLES))
super(WindowPygame, self).create_window()
# set mouse visibility
self._set_cursor_state(self.show_cursor)
# if we are on android platform, automatically create hooks
if android:
from kivy.support import install_android
install_android()
def close(self):
pygame.display.quit()
super(WindowPygame, self).close()
def on_title(self, instance, value):
if self.initialized:
pygame.display.set_caption(self.title)
def set_icon(self, filename):
if not exists(filename):
return False
try:
if platform == 'win':
try:
if self._set_icon_win(filename):
return True
except:
# fallback on standard loading then.
pass
# for all others platform, or if the ico is not available, use the
# default way to set it.
self._set_icon_standard(filename)
super(WindowPygame, self).set_icon(filename)
except:
Logger.exception('WinPygame: unable to set icon')
def _set_icon_standard(self, filename):
if PY2:
try:
im = pygame.image.load(filename)
except UnicodeEncodeError:
im = pygame.image.load(filename.encode('utf8'))
else:
im = pygame.image.load(filename)
if im is None:
raise Exception('Unable to load window icon (not found)')
pygame.display.set_icon(im)
def _set_icon_win(self, filename):
# ensure the window ico is ended by ico
if not filename.endswith('.ico'):
filename = '{}.ico'.format(filename.rsplit('.', 1)[0])
if not exists(filename):
return False
import win32api
import win32gui
import win32con
hwnd = pygame.display.get_wm_info()['window']
icon_big = win32gui.LoadImage(
None, filename, win32con.IMAGE_ICON,
48, 48, win32con.LR_LOADFROMFILE)
icon_small = win32gui.LoadImage(
None, filename, win32con.IMAGE_ICON,
16, 16, win32con.LR_LOADFROMFILE)
win32api.SendMessage(
hwnd, win32con.WM_SETICON, win32con.ICON_SMALL, icon_small)
win32api.SendMessage(
hwnd, win32con.WM_SETICON, win32con.ICON_BIG, icon_big)
return True
def _set_cursor_state(self, value):
pygame.mouse.set_visible(value)
def screenshot(self, *largs, **kwargs):
global glReadPixels, GL_RGBA, GL_UNSIGNED_BYTE
filename = super(WindowPygame, self).screenshot(*largs, **kwargs)
if filename is None:
return None
if glReadPixels is None:
from kivy.graphics.opengl import (glReadPixels, GL_RGBA,
GL_UNSIGNED_BYTE)
width, height = self.system_size
data = glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE)
data = bytes(bytearray(data))
surface = pygame.image.fromstring(data, (width, height), 'RGBA', True)
pygame.image.save(surface, filename)
Logger.debug('Window: Screenshot saved at <%s>' % filename)
return filename
def flip(self):
pygame.display.flip()
super(WindowPygame, self).flip()
def mainloop(self):
for event in pygame.event.get():
# kill application (SIG_TERM)
if event.type == pygame.QUIT:
if self.dispatch('on_request_close'):
continue
EventLoop.quit = True
self.close()
# mouse move
elif event.type == pygame.MOUSEMOTION:
x, y = event.pos
self.mouse_pos = x, self.system_size[1] - y
# don't dispatch motion if no button are pressed
if event.buttons == (0, 0, 0):
continue
self._mouse_x = x
self._mouse_y = y
self._mouse_meta = self.modifiers
self.dispatch('on_mouse_move', x, y, self.modifiers)
# mouse action
elif event.type in (pygame.MOUSEBUTTONDOWN,
pygame.MOUSEBUTTONUP):
self._pygame_update_modifiers()
x, y = event.pos
btn = 'left'
if event.button == 3:
btn = 'right'
elif event.button == 2:
btn = 'middle'
elif event.button == 4:
btn = 'scrolldown'
elif event.button == 5:
btn = 'scrollup'
elif event.button == 6:
btn = 'scrollright'
elif event.button == 7:
btn = 'scrollleft'
eventname = 'on_mouse_down'
if event.type == pygame.MOUSEBUTTONUP:
eventname = 'on_mouse_up'
self._mouse_x = x
self._mouse_y = y
self._mouse_meta = self.modifiers
self._mouse_btn = btn
self._mouse_down = eventname == 'on_mouse_down'
self.dispatch(eventname, x, y, btn, self.modifiers)
# joystick action
elif event.type == pygame.JOYAXISMOTION:
self.dispatch('on_joy_axis', event.joy, event.axis,
event.value)
elif event.type == pygame.JOYHATMOTION:
self.dispatch('on_joy_hat', event.joy, event.hat, event.value)
elif event.type == pygame.JOYBALLMOTION:
self.dispatch('on_joy_ball', event.joy, event.ballid,
event.rel[0], event.rel[1])
elif event.type == pygame.JOYBUTTONDOWN:
self.dispatch('on_joy_button_down', event.joy, event.button)
elif event.type == pygame.JOYBUTTONUP:
self.dispatch('on_joy_button_up', event.joy, event.button)
# keyboard action
elif event.type in (pygame.KEYDOWN, pygame.KEYUP):
self._pygame_update_modifiers(event.mod)
# atm, don't handle keyup
if event.type == pygame.KEYUP:
self.dispatch('on_key_up', event.key,
event.scancode)
continue
# don't dispatch more key if down event is accepted
if self.dispatch('on_key_down', event.key,
event.scancode, event.unicode,
self.modifiers):
continue
self.dispatch('on_keyboard', event.key,
event.scancode, event.unicode,
self.modifiers)
# video resize
elif event.type == pygame.VIDEORESIZE:
self._size = event.size
self.update_viewport()
elif event.type == pygame.VIDEOEXPOSE:
self.canvas.ask_update()
# ignored event
elif event.type == pygame.ACTIVEEVENT:
pass
# drop file (pygame patch needed)
elif event.type == pygame.USEREVENT and \
hasattr(pygame, 'USEREVENT_DROPFILE') and \
event.code == pygame.USEREVENT_DROPFILE:
drop_x, drop_y = pygame.mouse.get_pos()
self.dispatch('on_drop_file', event.filename, drop_x, drop_y)
'''
# unhandled event !
else:
Logger.debug('WinPygame: Unhandled event %s' % str(event))
'''
if not pygame.display.get_active():
pygame.time.wait(100)
#
# Pygame wrapper
#
def _pygame_set_mode(self, size=None):
if size is None:
size = self.size
if self.fullscreen == 'auto':
pygame.display.set_mode((0, 0), self.flags)
else:
pygame.display.set_mode(size, self.flags)
def _pygame_update_modifiers(self, mods=None):
# Available mod, from dir(pygame)
# 'KMOD_ALT', 'KMOD_CAPS', 'KMOD_CTRL', 'KMOD_LALT',
# 'KMOD_LCTRL', 'KMOD_LMETA', 'KMOD_LSHIFT', 'KMOD_META',
# 'KMOD_MODE', 'KMOD_NONE'
if mods is None:
mods = pygame.key.get_mods()
self._modifiers = []
if mods & (pygame.KMOD_SHIFT | pygame.KMOD_LSHIFT):
self._modifiers.append('shift')
if mods & (pygame.KMOD_ALT | pygame.KMOD_LALT):
self._modifiers.append('alt')
if mods & (pygame.KMOD_CTRL | pygame.KMOD_LCTRL):
self._modifiers.append('ctrl')
if mods & (pygame.KMOD_META | pygame.KMOD_LMETA):
self._modifiers.append('meta')
def request_keyboard(
self, callback, target, input_type='text', keyboard_suggestions=True
):
keyboard = super(WindowPygame, self).request_keyboard(
callback, target, input_type, keyboard_suggestions)
if android and not self.allow_vkeyboard:
android.show_keyboard(target, input_type)
return keyboard
def release_keyboard(self, *largs):
super(WindowPygame, self).release_keyboard(*largs)
if android:
android.hide_keyboard()
return True
@@ -0,0 +1,999 @@
# found a way to include it more easily.
'''
SDL2 Window
===========
Windowing provider directly based on our own wrapped version of SDL.
TODO:
- fix keys
- support scrolling
- clean code
- manage correctly all sdl events
'''
__all__ = ('WindowSDL', )
from os.path import join
import sys
from typing import Optional
from kivy import kivy_data_dir
from kivy.logger import Logger
from kivy.base import EventLoop
from kivy.clock import Clock
from kivy.config import Config
from kivy.core.window import WindowBase
try:
from kivy.core.window._window_sdl2 import _WindowSDL2Storage
except ImportError:
from kivy.core import handle_win_lib_import_error
handle_win_lib_import_error(
'window', 'sdl2', 'kivy.core.window._window_sdl2')
raise
from kivy.input.provider import MotionEventProvider
from kivy.input.motionevent import MotionEvent
from kivy.resources import resource_find
from kivy.utils import platform, deprecated
from kivy.compat import unichr
from collections import deque
# SDL_keycode.h, https://wiki.libsdl.org/SDL_Keymod
KMOD_NONE = 0x0000
KMOD_LSHIFT = 0x0001
KMOD_RSHIFT = 0x0002
KMOD_LCTRL = 0x0040
KMOD_RCTRL = 0x0080
KMOD_LALT = 0x0100
KMOD_RALT = 0x0200
KMOD_LGUI = 0x0400
KMOD_RGUI = 0x0800
KMOD_NUM = 0x1000
KMOD_CAPS = 0x2000
KMOD_MODE = 0x4000
SDLK_SHIFTL = 1073742049
SDLK_SHIFTR = 1073742053
SDLK_LCTRL = 1073742048
SDLK_RCTRL = 1073742052
SDLK_LALT = 1073742050
SDLK_RALT = 1073742054
SDLK_LEFT = 1073741904
SDLK_RIGHT = 1073741903
SDLK_UP = 1073741906
SDLK_DOWN = 1073741905
SDLK_HOME = 1073741898
SDLK_END = 1073741901
SDLK_PAGEUP = 1073741899
SDLK_PAGEDOWN = 1073741902
SDLK_SUPER = 1073742051
SDLK_CAPS = 1073741881
SDLK_INSERT = 1073741897
SDLK_KEYPADNUM = 1073741907
SDLK_KP_DIVIDE = 1073741908
SDLK_KP_MULTIPLY = 1073741909
SDLK_KP_MINUS = 1073741910
SDLK_KP_PLUS = 1073741911
SDLK_KP_ENTER = 1073741912
SDLK_KP_1 = 1073741913
SDLK_KP_2 = 1073741914
SDLK_KP_3 = 1073741915
SDLK_KP_4 = 1073741916
SDLK_KP_5 = 1073741917
SDLK_KP_6 = 1073741918
SDLK_KP_7 = 1073741919
SDLK_KP_8 = 1073741920
SDLK_KP_9 = 1073741921
SDLK_KP_0 = 1073741922
SDLK_KP_DOT = 1073741923
SDLK_F1 = 1073741882
SDLK_F2 = 1073741883
SDLK_F3 = 1073741884
SDLK_F4 = 1073741885
SDLK_F5 = 1073741886
SDLK_F6 = 1073741887
SDLK_F7 = 1073741888
SDLK_F8 = 1073741889
SDLK_F9 = 1073741890
SDLK_F10 = 1073741891
SDLK_F11 = 1073741892
SDLK_F12 = 1073741893
SDLK_F13 = 1073741894
SDLK_F14 = 1073741895
SDLK_F15 = 1073741896
class SDL2MotionEvent(MotionEvent):
def __init__(self, *args, **kwargs):
kwargs.setdefault('is_touch', True)
kwargs.setdefault('type_id', 'touch')
super().__init__(*args, **kwargs)
self.profile = ('pos', 'pressure')
def depack(self, args):
self.sx, self.sy, self.pressure = args
super().depack(args)
class SDL2MotionEventProvider(MotionEventProvider):
win = None
q = deque()
touchmap = {}
def update(self, dispatch_fn):
touchmap = self.touchmap
while True:
try:
value = self.q.pop()
except IndexError:
return
action, fid, x, y, pressure = value
y = 1 - y
if fid not in touchmap:
touchmap[fid] = me = SDL2MotionEvent(
'sdl', fid, (x, y, pressure)
)
else:
me = touchmap[fid]
me.move((x, y, pressure))
if action == 'fingerdown':
dispatch_fn('begin', me)
elif action == 'fingerup':
me.update_time_end()
dispatch_fn('end', me)
del touchmap[fid]
else:
dispatch_fn('update', me)
class WindowSDL(WindowBase):
_win_dpi_watch: Optional['_WindowsSysDPIWatch'] = None
_do_resize_ev = None
managed_textinput = True
def __init__(self, **kwargs):
self._pause_loop = False
self._cursor_entered = False
self._drop_pos = None
self._win = _WindowSDL2Storage()
super(WindowSDL, self).__init__()
self.titlebar_widget = None
self._mouse_x = self._mouse_y = -1
self._meta_keys = (
KMOD_LCTRL, KMOD_RCTRL, KMOD_RSHIFT,
KMOD_LSHIFT, KMOD_RALT, KMOD_LALT, KMOD_LGUI,
KMOD_RGUI, KMOD_NUM, KMOD_CAPS, KMOD_MODE)
self.command_keys = {
27: 'escape',
9: 'tab',
8: 'backspace',
13: 'enter',
127: 'del',
271: 'enter',
273: 'up',
274: 'down',
275: 'right',
276: 'left',
278: 'home',
279: 'end',
280: 'pgup',
281: 'pgdown'}
self._mouse_buttons_down = set()
self.key_map = {SDLK_LEFT: 276, SDLK_RIGHT: 275, SDLK_UP: 273,
SDLK_DOWN: 274, SDLK_HOME: 278, SDLK_END: 279,
SDLK_PAGEDOWN: 281, SDLK_PAGEUP: 280, SDLK_SHIFTR: 303,
SDLK_SHIFTL: 304, SDLK_SUPER: 309, SDLK_LCTRL: 305,
SDLK_RCTRL: 306, SDLK_LALT: 308, SDLK_RALT: 307,
SDLK_CAPS: 301, SDLK_INSERT: 277, SDLK_F1: 282,
SDLK_F2: 283, SDLK_F3: 284, SDLK_F4: 285, SDLK_F5: 286,
SDLK_F6: 287, SDLK_F7: 288, SDLK_F8: 289, SDLK_F9: 290,
SDLK_F10: 291, SDLK_F11: 292, SDLK_F12: 293,
SDLK_F13: 294, SDLK_F14: 295, SDLK_F15: 296,
SDLK_KEYPADNUM: 300, SDLK_KP_DIVIDE: 267,
SDLK_KP_MULTIPLY: 268, SDLK_KP_MINUS: 269,
SDLK_KP_PLUS: 270, SDLK_KP_ENTER: 271,
SDLK_KP_DOT: 266, SDLK_KP_0: 256, SDLK_KP_1: 257,
SDLK_KP_2: 258, SDLK_KP_3: 259, SDLK_KP_4: 260,
SDLK_KP_5: 261, SDLK_KP_6: 262, SDLK_KP_7: 263,
SDLK_KP_8: 264, SDLK_KP_9: 265}
if platform == 'ios':
# XXX ios keyboard suck, when backspace is hit, the delete
# keycode is sent. fix it.
self.key_map[127] = 8
elif platform == 'android':
# map android back button to escape
self.key_map[1073742094] = 27
self.bind(minimum_width=self._set_minimum_size,
minimum_height=self._set_minimum_size)
self.bind(allow_screensaver=self._set_allow_screensaver)
self.bind(always_on_top=self._set_always_on_top)
def get_window_info(self):
return self._win.get_window_info()
def _set_minimum_size(self, *args):
minimum_width = self.minimum_width
minimum_height = self.minimum_height
if minimum_width and minimum_height:
self._win.set_minimum_size(minimum_width, minimum_height)
elif minimum_width or minimum_height:
Logger.warning(
'Both Window.minimum_width and Window.minimum_height must be '
'bigger than 0 for the size restriction to take effect.')
def _set_always_on_top(self, *args):
self._win.set_always_on_top(self.always_on_top)
def _set_allow_screensaver(self, *args):
self._win.set_allow_screensaver(self.allow_screensaver)
def _event_filter(self, action, *largs):
from kivy.app import App
if action == 'app_terminating':
EventLoop.quit = True
elif action == 'app_lowmemory':
self.dispatch('on_memorywarning')
elif action == 'app_willenterbackground':
from kivy.base import stopTouchApp
app = App.get_running_app()
if not app:
Logger.info('WindowSDL: No running App found, pause.')
elif not app.dispatch('on_pause'):
if platform == 'android':
Logger.info(
'WindowSDL: App stopped, on_pause() returned False.')
from android import mActivity
mActivity.finishAndRemoveTask()
else:
Logger.info(
'WindowSDL: App doesn\'t support pause mode, stop.')
stopTouchApp()
return 0
self._pause_loop = True
elif action == 'app_didenterforeground':
# on iOS, the did enter foreground is launched at the start
# of the application. in our case, we want it only when the app
# is resumed
if self._pause_loop:
self._pause_loop = False
app = App.get_running_app()
if app:
app.dispatch('on_resume')
elif action == 'windowresized':
self._size = largs
self._win.resize_window(*self._size)
# Force kivy to render the frame now, so that the canvas is drawn.
EventLoop.idle()
return 0
def create_window(self, *largs):
if self._fake_fullscreen:
if not self.borderless:
self.fullscreen = self._fake_fullscreen = False
elif not self.fullscreen or self.fullscreen == 'auto':
self.custom_titlebar = \
self.borderless = self._fake_fullscreen = False
elif self.custom_titlebar:
if platform == 'win':
# use custom behavior
# To handle aero snapping and rounded corners
self.borderless = False
if self.fullscreen == 'fake':
self.borderless = self._fake_fullscreen = True
Logger.warning("The 'fake' fullscreen option has been "
"deprecated, use Window.borderless or the "
"borderless Config option instead.")
if not self.initialized:
if self.position == 'auto':
pos = None, None
elif self.position == 'custom':
pos = self.left, self.top
# ensure we have an event filter
self._win.set_event_filter(self._event_filter)
# setup window
w, h = self.system_size
resizable = Config.getboolean('graphics', 'resizable')
state = (Config.get('graphics', 'window_state')
if self._is_desktop else None)
self.system_size = self._win.setup_window(
pos[0], pos[1], w, h, self.borderless,
self.fullscreen, resizable, state,
self.get_gl_backend_name())
# We don't have a density or dpi yet set, so let's ask for an update
self._update_density_and_dpi()
# never stay with a None pos, application using w.center
# will be fired.
self._pos = (0, 0)
self._set_minimum_size()
self._set_allow_screensaver()
self._set_always_on_top()
if state == 'hidden':
self._focus = False
else:
w, h = self.system_size
self._win.resize_window(w, h)
if platform == 'win':
if self.custom_titlebar:
# check dragging+resize or just dragging
if Config.getboolean('graphics', 'resizable'):
import win32con
import ctypes
self._win.set_border_state(False)
# make windows dispatch,
# WM_NCCALCSIZE explicitly
ctypes.windll.user32.SetWindowPos(
self._win.get_window_info().window,
win32con.HWND_TOP,
*self._win.get_window_pos(),
*self.system_size,
win32con.SWP_FRAMECHANGED
)
else:
self._win.set_border_state(True)
else:
self._win.set_border_state(self.borderless)
else:
self._win.set_border_state(self.borderless
or self.custom_titlebar)
self._win.set_fullscreen_mode(self.fullscreen)
super(WindowSDL, self).create_window()
# set mouse visibility
self._set_cursor_state(self.show_cursor)
if self.initialized:
return
# auto add input provider
Logger.info('Window: auto add sdl2 input provider')
SDL2MotionEventProvider.win = self
EventLoop.add_input_provider(SDL2MotionEventProvider('sdl', ''))
# set window icon before calling set_mode
try:
filename_icon = self.icon or Config.get('kivy', 'window_icon')
if filename_icon == '':
logo_size = 32
if platform == 'macosx':
logo_size = 512
elif platform == 'win':
logo_size = 64
filename_icon = 'kivy-icon-{}.png'.format(logo_size)
filename_icon = resource_find(
join(kivy_data_dir, 'logo', filename_icon))
self.set_icon(filename_icon)
except:
Logger.exception('Window: cannot set icon')
if platform == 'win' and self._win_dpi_watch is None:
self._win_dpi_watch = _WindowsSysDPIWatch(window=self)
self._win_dpi_watch.start()
def _update_density_and_dpi(self):
if platform == 'win':
from ctypes import windll
self._density = 1.
try:
hwnd = windll.user32.GetActiveWindow()
self.dpi = float(windll.user32.GetDpiForWindow(hwnd))
self._density = self.dpi / 96
except AttributeError:
pass
else:
self._density = self._win._get_gl_size()[0] / self._size[0]
if self._is_desktop:
self.dpi = self._density * 96.
def close(self):
self._win.teardown_window()
super(WindowSDL, self).close()
if self._win_dpi_watch is not None:
self._win_dpi_watch.stop()
self._win_dpi_watch = None
self.initialized = False
def maximize(self):
if self._is_desktop:
self._win.maximize_window()
else:
Logger.warning('Window: maximize() is used only on desktop OSes.')
def minimize(self):
if self._is_desktop:
self._win.minimize_window()
else:
Logger.warning('Window: minimize() is used only on desktop OSes.')
def restore(self):
if self._is_desktop:
self._win.restore_window()
else:
Logger.warning('Window: restore() is used only on desktop OSes.')
def hide(self):
if self._is_desktop:
self._win.hide_window()
else:
Logger.warning('Window: hide() is used only on desktop OSes.')
def show(self):
if self._is_desktop:
self._win.show_window()
else:
Logger.warning('Window: show() is used only on desktop OSes.')
def raise_window(self):
if self._is_desktop:
self._win.raise_window()
else:
Logger.warning('Window: show() is used only on desktop OSes.')
def set_title(self, title):
self._win.set_window_title(title)
def set_icon(self, filename):
self._win.set_window_icon(str(filename))
def screenshot(self, *largs, **kwargs):
filename = super(WindowSDL, self).screenshot(*largs, **kwargs)
if filename is None:
return
from kivy.graphics.opengl import glReadPixels, GL_RGB, GL_UNSIGNED_BYTE
width, height = self.size
data = glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE)
self._win.save_bytes_in_png(filename, data, width, height)
Logger.debug('Window: Screenshot saved at <%s>' % filename)
return filename
def flip(self):
self._win.flip()
super(WindowSDL, self).flip()
def set_system_cursor(self, cursor_name):
result = self._win.set_system_cursor(cursor_name)
return result
def _get_window_pos(self):
return self._win.get_window_pos()
def _set_window_pos(self, x, y):
self._win.set_window_pos(x, y)
def _get_window_opacity(self):
return self._win.get_window_opacity()
def _set_window_opacity(self, opacity):
if self.opacity != opacity:
return self._win.set_window_opacity(opacity)
# Transparent Window background
def _is_shaped(self):
return self._win.is_window_shaped()
def _set_shape(self, shape_image, mode='default',
cutoff=False, color_key=None):
modes = ('default', 'binalpha', 'reversebinalpha', 'colorkey')
color_key = color_key or (0, 0, 0, 1)
if mode not in modes:
Logger.warning(
'Window: shape mode can be only '
'{}'.format(', '.join(modes))
)
return
if not isinstance(color_key, (tuple, list)):
return
if len(color_key) not in (3, 4):
return
if len(color_key) == 3:
color_key = (color_key[0], color_key[1], color_key[2], 1)
Logger.warning(
'Window: Shape color_key must be only tuple or list'
)
return
color_key = (
color_key[0] * 255,
color_key[1] * 255,
color_key[2] * 255,
color_key[3] * 255
)
assert cutoff in (1, 0)
shape_image = shape_image or Config.get('kivy', 'window_shape')
shape_image = resource_find(shape_image) or shape_image
self._win.set_shape(shape_image, mode, cutoff, color_key)
def _get_shaped_mode(self):
return self._win.get_shaped_mode()
def _set_shaped_mode(self, value):
self._set_shape(
shape_image=self.shape_image,
mode=value, cutoff=self.shape_cutoff,
color_key=self.shape_color_key
)
return self._win.get_shaped_mode()
# twb end
def _set_cursor_state(self, value):
self._win._set_cursor_state(value)
def _fix_mouse_pos(self, x, y):
self.mouse_pos = (
x * self._density,
(self.system_size[1] - 1 - y) * self._density
)
return x, y
def mainloop(self):
# for android/iOS, we don't want to have any event nor executing our
# main loop while the pause is going on. This loop wait any event (not
# handled by the event filter), and remove them from the queue.
# Nothing happen during the pause on iOS, except gyroscope value sent
# over joystick. So it's safe.
while self._pause_loop:
self._win.wait_event()
if not self._pause_loop:
break
event = self._win.poll()
if event is None:
continue
# A drop is send while the app is still in pause.loop
# we need to dispatch it
action, args = event[0], event[1:]
if action.startswith('drop'):
self._dispatch_drop_event(action, args)
# app_terminating event might be received while the app is paused
# in this case EventLoop.quit will be set at _event_filter
elif EventLoop.quit:
return
while True:
event = self._win.poll()
if event is False:
break
if event is None:
continue
action, args = event[0], event[1:]
if action == 'quit':
if self.dispatch('on_request_close'):
continue
EventLoop.quit = True
break
elif action in ('fingermotion', 'fingerdown', 'fingerup'):
# for finger, pass the raw event to SDL motion event provider
# XXX this is problematic. On OSX, it generates touches with 0,
# 0 coordinates, at the same times as mouse. But it works.
# We have a conflict of using either the mouse or the finger.
# Right now, we have no mechanism that we could use to know
# which is the preferred one for the application.
if platform in ('ios', 'android'):
SDL2MotionEventProvider.q.appendleft(event)
pass
elif action == 'mousemotion':
x, y = args
x, y = self._fix_mouse_pos(x, y)
self._mouse_x = x
self._mouse_y = y
if not self._cursor_entered:
self._cursor_entered = True
self.dispatch('on_cursor_enter')
# don't dispatch motion if no button are pressed
if len(self._mouse_buttons_down) == 0:
continue
self._mouse_meta = self.modifiers
self.dispatch('on_mouse_move', x, y, self.modifiers)
elif action in ('mousebuttondown', 'mousebuttonup'):
x, y, button = args
x, y = self._fix_mouse_pos(x, y)
self._mouse_x = x
self._mouse_y = y
if not self._cursor_entered:
self._cursor_entered = True
self.dispatch('on_cursor_enter')
btn = 'left'
if button == 3:
btn = 'right'
elif button == 2:
btn = 'middle'
elif button == 4:
btn = "mouse4"
elif button == 5:
btn = "mouse5"
eventname = 'on_mouse_down'
self._mouse_buttons_down.add(button)
if action == 'mousebuttonup':
eventname = 'on_mouse_up'
self._mouse_buttons_down.remove(button)
self.dispatch(eventname, x, y, btn, self.modifiers)
elif action.startswith('mousewheel'):
x, y = self._win.get_relative_mouse_pos()
if not self._collide_and_dispatch_cursor_enter(x, y):
# Ignore if the cursor position is on the window title bar
# or on its edges
continue
self._update_modifiers()
x, y, button = args
btn = 'scrolldown'
if action.endswith('up'):
btn = 'scrollup'
elif action.endswith('right'):
btn = 'scrollright'
elif action.endswith('left'):
btn = 'scrollleft'
self._mouse_meta = self.modifiers
self._mouse_btn = btn
# times = x if y == 0 else y
# times = min(abs(times), 100)
# for k in range(times):
self._mouse_down = True
self.dispatch('on_mouse_down',
self._mouse_x, self._mouse_y, btn, self.modifiers)
self._mouse_down = False
self.dispatch('on_mouse_up',
self._mouse_x, self._mouse_y, btn, self.modifiers)
elif action.startswith('drop'):
self._dispatch_drop_event(action, args)
# video resize
elif action == 'windowresized':
self._size = self._win.window_size
# don't use trigger here, we want to delay the resize event
ev = self._do_resize_ev
if ev is None:
ev = Clock.schedule_once(self._do_resize, .1)
self._do_resize_ev = ev
else:
ev()
elif action == 'windowdisplaychanged':
Logger.info(f"WindowSDL: Window is now on display {args[0]}")
# The display has changed, so the density and dpi
# may have changed too.
self._update_density_and_dpi()
elif action == 'windowmoved':
self.dispatch('on_move')
elif action == 'windowrestored':
self.dispatch('on_restore')
self.canvas.ask_update()
elif action == 'windowexposed':
self.canvas.ask_update()
elif action == 'windowminimized':
self.dispatch('on_minimize')
if Config.getboolean('kivy', 'pause_on_minimize'):
self.do_pause()
elif action == 'windowmaximized':
self.dispatch('on_maximize')
elif action == 'windowhidden':
self.dispatch('on_hide')
elif action == 'windowshown':
self.dispatch('on_show')
elif action == 'windowfocusgained':
self._focus = True
elif action == 'windowfocuslost':
self._focus = False
elif action == 'windowenter':
x, y = self._win.get_relative_mouse_pos()
self._collide_and_dispatch_cursor_enter(x, y)
elif action == 'windowleave':
self._cursor_entered = False
self.dispatch('on_cursor_leave')
elif action == 'joyaxismotion':
stickid, axisid, value = args
self.dispatch('on_joy_axis', stickid, axisid, value)
elif action == 'joyhatmotion':
stickid, hatid, value = args
self.dispatch('on_joy_hat', stickid, hatid, value)
elif action == 'joyballmotion':
stickid, ballid, xrel, yrel = args
self.dispatch('on_joy_ball', stickid, ballid, xrel, yrel)
elif action == 'joybuttondown':
stickid, buttonid = args
self.dispatch('on_joy_button_down', stickid, buttonid)
elif action == 'joybuttonup':
stickid, buttonid = args
self.dispatch('on_joy_button_up', stickid, buttonid)
elif action in ('keydown', 'keyup'):
mod, key, scancode, kstr = args
try:
key = self.key_map[key]
except KeyError:
pass
if action == 'keydown':
self._update_modifiers(mod, key)
else:
# ignore the key, it has been released
self._update_modifiers(mod)
# if mod in self._meta_keys:
if (key not in self._modifiers and
key not in self.command_keys.keys()):
try:
kstr_chr = unichr(key)
try:
# On android, there is no 'encoding' attribute.
# On other platforms, if stdout is redirected,
# 'encoding' may be None
encoding = getattr(sys.stdout, 'encoding',
'utf8') or 'utf8'
kstr_chr.encode(encoding)
kstr = kstr_chr
except UnicodeError:
pass
except ValueError:
pass
# if 'shift' in self._modifiers and key\
# not in self.command_keys.keys():
# return
if action == 'keyup':
self.dispatch('on_key_up', key, scancode)
continue
# don't dispatch more key if down event is accepted
if self.dispatch('on_key_down', key,
scancode, kstr,
self.modifiers):
continue
self.dispatch('on_keyboard', key,
scancode, kstr,
self.modifiers)
elif action == 'textinput':
text = args[0]
self.dispatch('on_textinput', text)
elif action == 'textedit':
text = args[0]
self.dispatch('on_textedit', text)
# unhandled event !
else:
Logger.trace('WindowSDL: Unhandled event %s' % str(event))
def _dispatch_drop_event(self, action, args):
x, y = (0, 0) if self._drop_pos is None else self._drop_pos
if action == 'dropfile':
self.dispatch('on_drop_file', args[0], x, y)
elif action == 'droptext':
self.dispatch('on_drop_text', args[0], x, y)
elif action == 'dropbegin':
self._drop_pos = x, y = self._win.get_relative_mouse_pos()
self._collide_and_dispatch_cursor_enter(x, y)
self.dispatch('on_drop_begin', x, y)
elif action == 'dropend':
self._drop_pos = None
self.dispatch('on_drop_end', x, y)
def _collide_and_dispatch_cursor_enter(self, x, y):
# x, y are relative to window left/top position
w, h = self._win.window_size
if 0 <= x < w and 0 <= y < h:
self._mouse_x, self._mouse_y = self._fix_mouse_pos(x, y)
if not self._cursor_entered:
self._cursor_entered = True
self.dispatch('on_cursor_enter')
return True
def _do_resize(self, dt):
Logger.debug('Window: Resize window to %s' % str(self.size))
self._win.resize_window(*self._size)
self.dispatch('on_pre_resize', *self.size)
def do_pause(self):
# should go to app pause mode (desktop style)
from kivy.app import App
from kivy.base import stopTouchApp
app = App.get_running_app()
if not app:
Logger.info('WindowSDL: No running App found, pause.')
elif not app.dispatch('on_pause'):
Logger.info('WindowSDL: App doesn\'t support pause mode, stop.')
stopTouchApp()
return
# XXX FIXME wait for sdl resume
while True:
event = self._win.poll()
if event is False:
continue
if event is None:
continue
action, args = event[0], event[1:]
if action == 'quit':
EventLoop.quit = True
break
elif action == 'app_willenterforeground':
break
elif action == 'windowrestored':
break
if app:
app.dispatch('on_resume')
def _update_modifiers(self, mods=None, key=None):
if mods is None and key is None:
return
modifiers = set()
if mods is not None:
if mods & (KMOD_RSHIFT | KMOD_LSHIFT):
modifiers.add('shift')
if mods & (KMOD_RALT | KMOD_LALT | KMOD_MODE):
modifiers.add('alt')
if mods & (KMOD_RCTRL | KMOD_LCTRL):
modifiers.add('ctrl')
if mods & (KMOD_RGUI | KMOD_LGUI):
modifiers.add('meta')
if mods & KMOD_NUM:
modifiers.add('numlock')
if mods & KMOD_CAPS:
modifiers.add('capslock')
if key is not None:
if key in (KMOD_RSHIFT, KMOD_LSHIFT):
modifiers.add('shift')
if key in (KMOD_RALT, KMOD_LALT, KMOD_MODE):
modifiers.add('alt')
if key in (KMOD_RCTRL, KMOD_LCTRL):
modifiers.add('ctrl')
if key in (KMOD_RGUI, KMOD_LGUI):
modifiers.add('meta')
if key == KMOD_NUM:
modifiers.add('numlock')
if key == KMOD_CAPS:
modifiers.add('capslock')
self._modifiers = list(modifiers)
return
def request_keyboard(
self, callback, target, input_type='text', keyboard_suggestions=True
):
self._sdl_keyboard = super(WindowSDL, self).\
request_keyboard(
callback, target, input_type, keyboard_suggestions
)
self._win.show_keyboard(
self._system_keyboard,
self.softinput_mode,
input_type,
keyboard_suggestions,
)
Clock.schedule_interval(self._check_keyboard_shown, 1 / 5.)
return self._sdl_keyboard
def release_keyboard(self, *largs):
super(WindowSDL, self).release_keyboard(*largs)
self._win.hide_keyboard()
self._sdl_keyboard = None
return True
def _check_keyboard_shown(self, dt):
if self._sdl_keyboard is None:
return False
if not self._win.is_keyboard_shown():
self._sdl_keyboard.release()
def map_key(self, original_key, new_key):
self.key_map[original_key] = new_key
def unmap_key(self, key):
if key in self.key_map:
del self.key_map[key]
def grab_mouse(self):
self._win.grab_mouse(True)
def ungrab_mouse(self):
self._win.grab_mouse(False)
def set_custom_titlebar(self, titlebar_widget):
if not self.custom_titlebar:
Logger.warning("Window: Window.custom_titlebar not set to True… "
"can't set custom titlebar")
return
self.titlebar_widget = titlebar_widget
return self._win.set_custom_titlebar(self.titlebar_widget) == 0
class _WindowsSysDPIWatch:
hwnd = None
new_windProc = None
old_windProc = None
window: WindowBase = None
def __init__(self, window: WindowBase):
self.window = window
def start(self):
from kivy.input.providers.wm_common import WNDPROC, \
SetWindowLong_WndProc_wrapper
from ctypes import windll
self.hwnd = windll.user32.GetActiveWindow()
# inject our own handler to handle messages before window manager
self.new_windProc = WNDPROC(self._wnd_proc)
self.old_windProc = SetWindowLong_WndProc_wrapper(
self.hwnd, self.new_windProc)
def stop(self):
from kivy.input.providers.wm_common import \
SetWindowLong_WndProc_wrapper
if self.hwnd is None:
return
self.new_windProc = SetWindowLong_WndProc_wrapper(
self.hwnd, self.old_windProc)
self.hwnd = self.new_windProc = self.old_windProc = None
def _wnd_proc(self, hwnd, msg, wParam, lParam):
from kivy.input.providers.wm_common import WM_DPICHANGED, WM_NCCALCSIZE
from ctypes import windll
if msg == WM_DPICHANGED:
def clock_callback(*args):
if x_dpi != y_dpi:
raise ValueError(
'Can only handle DPI that are same for x and y')
self.window.dpi = x_dpi
x_dpi = wParam & 0xFFFF
y_dpi = wParam >> 16
Clock.schedule_once(clock_callback, -1)
elif Config.getboolean('graphics', 'resizable') \
and msg == WM_NCCALCSIZE and self.window.custom_titlebar:
return 0
return windll.user32.CallWindowProcW(
self.old_windProc, hwnd, msg, wParam, lParam)