Ajout du GUI

This commit is contained in:
thatscringebro
2022-08-08 16:31:52 -04:00
parent db362ccdca
commit abd15f28b6
851 changed files with 99957 additions and 1 deletions

247
kivy/core/__init__.py Normal file
View File

@@ -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))

Binary file not shown.

244
kivy/core/audio/__init__.py Normal file
View File

@@ -0,0 +1,244 @@
'''
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.'''
#
# deprecated
#
def _get_status(self):
return self.state
status = AliasProperty(
_get_status, None, bind=('state', ), deprecated=True)
'''
.. deprecated:: 1.3.0
Use :attr:`state` instead.
'''
def _get_filename(self):
return self.source
filename = AliasProperty(
_get_filename, None, bind=('source', ), deprecated=True)
'''
.. deprecated:: 1.3.0
Use :attr:`source` instead.
'''
__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)

Binary file not shown.

View File

@@ -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)

View File

@@ -0,0 +1,72 @@
'''
AudioAvplayer: implementation of Sound using pyobjus / AVFoundation.
Works on iOS / OSX.
'''
__all__ = ('SoundAvplayer', )
from kivy.core.audio import Sound, SoundLoader
from pyobjus import autoclass
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.play()
super(SoundAvplayer, self).play()
def stop(self):
if not self._avplayer:
return
self._avplayer.stop()
super(SoundAvplayer, self).stop()
def seek(self, position):
if not self._avplayer:
return
self._avplayer.playAtTime_(float(position))
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()
SoundLoader.register(SoundAvplayer)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

Binary file not shown.

View File

@@ -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):
'''Initialise 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 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))

Binary file not shown.

Binary file not shown.

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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 initialised.
'''
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')

View File

@@ -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']

View File

@@ -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)

View File

@@ -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']

View File

@@ -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())

View File

@@ -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']

View File

@@ -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',)

View File

@@ -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

View File

@@ -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']

View File

@@ -0,0 +1,66 @@
'''
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
class ClipboardWindows(ClipboardBase):
def get(self, mimetype='text/plain'):
GetClipboardData = user32.GetClipboardData
GetClipboardData.argtypes = [wintypes.UINT]
GetClipboardData.restype = wintypes.HANDLE
user32.OpenClipboard(user32.GetActiveWindow())
# Standard Clipboard Format "1" is "CF_TEXT"
pcontents = GetClipboardData(13)
# 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 ''
data = c_wchar_p(pcontents).value.encode(self._encoding)
user32.CloseClipboard()
return data
def put(self, text, mimetype='text/plain'):
text = text.decode(self._encoding) # auto converted later
text += u'\x00'
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
CF_UNICODETEXT = 13
user32.OpenClipboard(user32.GetActiveWindow())
user32.EmptyClipboard()
hCd = GlobalAlloc(0, len(text) * ctypes.sizeof(ctypes.c_wchar))
# ignore null character for strSource pointer
msvcrt.wcscpy_s(c_wchar_p(hCd), len(text), c_wchar_p(text[:-1]))
SetClipboardData(CF_UNICODETEXT, hCd)
user32.CloseClipboard()
def get_types(self):
return ['text/plain']

View File

@@ -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)

View File

@@ -0,0 +1,29 @@
'''
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')
try:
import subprocess
p = subprocess.Popen(['xsel'], stdout=subprocess.PIPE)
p.communicate()
except:
raise
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)

83
kivy/core/gl/__init__.py Normal file
View File

@@ -0,0 +1,83 @@
# 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

Binary file not shown.

1002
kivy/core/image/__init__.py Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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)

View File

@@ -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)

123
kivy/core/image/img_pil.py Normal file
View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

1016
kivy/core/text/__init__.py Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

879
kivy/core/text/markup.py Normal file
View File

@@ -0,0 +1,879 @@
'''
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_x']
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 - 2 * 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):
xpad = options['padding_x']
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 = xpad
if halign == 'center':
x = int((w - lw) / 2.)
elif halign == 'right' or auto_halign_r:
x = max(0, int(w - lw - xpad))
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 else self.get_descent())
psp = script_pos
pph = wh
elif options['script'] == 'subscript':
script_pos = min(lh - wh, ((psp + pph) - wh)
if 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_x'] * 2 - margin))
chr = type(self.text)
ssize = textwidth(' ')
c = old_opts['split_str']
line_height = old_opts['line_height']
xpad, ypad = old_opts['padding_x'], old_opts['padding_y']
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 + 2 * xpad,
lh + 2 * 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] + 2 * xpad,
s[1] * line_height + 2 * ypad,
[LayoutLine(
0, 0, s[0], s[1], 1, 0,
[LayoutWord(old_opts, s[0], s[1], '..')])]
)
else:
s = textwidth('.')
return (
s[0] + 2 * xpad,
s[1] * line_height + 2 * 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 + 2 * xpad,
lh + 2 * 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 + 2 * xpad,
lh + 2 * 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 + 2 * xpad,
lh + 2 * ypad,
[LayoutLine(0, 0, lw, lh, 1, 0, line1)]
)

Binary file not shown.

View File

@@ -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

View File

@@ -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 collissions 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))

View File

@@ -0,0 +1,60 @@
'''
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):
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()
w, h = font.getsize(text)
return w, h
def get_cached_extents(self):
return self._select_font().getsize
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):
color = tuple([int(c * 255) for c in self.options['color']])
self._pil_draw.text((int(x), int(y)),
text, font=self._select_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

View File

@@ -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

View File

@@ -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()

219
kivy/core/video/__init__.py Normal file
View File

@@ -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)

Binary file not shown.

Binary file not shown.

View File

@@ -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)

View File

@@ -0,0 +1,446 @@
'''
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)
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
if src_pix_fmt == 'yuv420p':
self._out_fmt = 'yuv420p'
ffplayer.set_output_pix_fmt(self._out_fmt)
break
if self._ffplayer_need_quit:
ffplayer.close_player()
return
# wait until loaded or failed, shouldn't take long, but just to make
# sure metadata is available.
while not self._ffplayer_need_quit:
if ffplayer.get_metadata()['src_vid_size'] != (0, 0):
break
wait_for_wakeup(0.005)
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'
# it starts internally paused, but unpauses itself
ff_opts = {
'paused': True,
'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)

View File

@@ -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

View File

@@ -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

2460
kivy/core/window/__init__.py Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More