Ajout du GUI
This commit is contained in:
247
kivy/core/__init__.py
Normal file
247
kivy/core/__init__.py
Normal 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))
|
||||
BIN
kivy/core/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/core/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
244
kivy/core/audio/__init__.py
Normal file
244
kivy/core/audio/__init__.py
Normal 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)
|
||||
BIN
kivy/core/audio/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/core/audio/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/audio/__pycache__/audio_android.cpython-310.pyc
Normal file
BIN
kivy/core/audio/__pycache__/audio_android.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/audio/__pycache__/audio_avplayer.cpython-310.pyc
Normal file
BIN
kivy/core/audio/__pycache__/audio_avplayer.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/audio/__pycache__/audio_ffpyplayer.cpython-310.pyc
Normal file
BIN
kivy/core/audio/__pycache__/audio_ffpyplayer.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/audio/__pycache__/audio_gstplayer.cpython-310.pyc
Normal file
BIN
kivy/core/audio/__pycache__/audio_gstplayer.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/audio/__pycache__/audio_pygame.cpython-310.pyc
Normal file
BIN
kivy/core/audio/__pycache__/audio_pygame.cpython-310.pyc
Normal file
Binary file not shown.
104
kivy/core/audio/audio_android.py
Normal file
104
kivy/core/audio/audio_android.py
Normal 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)
|
||||
72
kivy/core/audio/audio_avplayer.py
Normal file
72
kivy/core/audio/audio_avplayer.py
Normal 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)
|
||||
185
kivy/core/audio/audio_ffpyplayer.py
Normal file
185
kivy/core/audio/audio_ffpyplayer.py
Normal 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)
|
||||
101
kivy/core/audio/audio_gstplayer.py
Normal file
101
kivy/core/audio/audio_gstplayer.py
Normal 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)
|
||||
127
kivy/core/audio/audio_pygame.py
Normal file
127
kivy/core/audio/audio_pygame.py
Normal 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)
|
||||
BIN
kivy/core/audio/audio_sdl2.cpython-310-x86_64-linux-gnu.so
Executable file
BIN
kivy/core/audio/audio_sdl2.cpython-310-x86_64-linux-gnu.so
Executable file
Binary file not shown.
151
kivy/core/camera/__init__.py
Normal file
151
kivy/core/camera/__init__.py
Normal 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))
|
||||
BIN
kivy/core/camera/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/core/camera/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/camera/__pycache__/camera_android.cpython-310.pyc
Normal file
BIN
kivy/core/camera/__pycache__/camera_android.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/camera/__pycache__/camera_gi.cpython-310.pyc
Normal file
BIN
kivy/core/camera/__pycache__/camera_gi.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/camera/__pycache__/camera_opencv.cpython-310.pyc
Normal file
BIN
kivy/core/camera/__pycache__/camera_opencv.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/camera/__pycache__/camera_picamera.cpython-310.pyc
Normal file
BIN
kivy/core/camera/__pycache__/camera_picamera.cpython-310.pyc
Normal file
Binary file not shown.
206
kivy/core/camera/camera_android.py
Normal file
206
kivy/core/camera/camera_android.py
Normal 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()
|
||||
170
kivy/core/camera/camera_gi.py
Normal file
170
kivy/core/camera/camera_gi.py
Normal 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()
|
||||
163
kivy/core/camera/camera_opencv.py
Normal file
163
kivy/core/camera/camera_opencv.py
Normal 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
|
||||
96
kivy/core/camera/camera_picamera.py
Normal file
96
kivy/core/camera/camera_picamera.py
Normal 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
|
||||
157
kivy/core/clipboard/__init__.py
Normal file
157
kivy/core/clipboard/__init__.py
Normal 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')
|
||||
BIN
kivy/core/clipboard/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/core/clipboard/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/clipboard/__pycache__/_clipboard_ext.cpython-310.pyc
Normal file
BIN
kivy/core/clipboard/__pycache__/_clipboard_ext.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
kivy/core/clipboard/__pycache__/clipboard_dummy.cpython-310.pyc
Normal file
BIN
kivy/core/clipboard/__pycache__/clipboard_dummy.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/clipboard/__pycache__/clipboard_gtk3.cpython-310.pyc
Normal file
BIN
kivy/core/clipboard/__pycache__/clipboard_gtk3.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
kivy/core/clipboard/__pycache__/clipboard_pygame.cpython-310.pyc
Normal file
BIN
kivy/core/clipboard/__pycache__/clipboard_pygame.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/clipboard/__pycache__/clipboard_sdl2.cpython-310.pyc
Normal file
BIN
kivy/core/clipboard/__pycache__/clipboard_sdl2.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
kivy/core/clipboard/__pycache__/clipboard_xclip.cpython-310.pyc
Normal file
BIN
kivy/core/clipboard/__pycache__/clipboard_xclip.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/clipboard/__pycache__/clipboard_xsel.cpython-310.pyc
Normal file
BIN
kivy/core/clipboard/__pycache__/clipboard_xsel.cpython-310.pyc
Normal file
Binary file not shown.
36
kivy/core/clipboard/_clipboard_ext.py
Normal file
36
kivy/core/clipboard/_clipboard_ext.py
Normal 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']
|
||||
BIN
kivy/core/clipboard/_clipboard_sdl2.cpython-310-x86_64-linux-gnu.so
Executable file
BIN
kivy/core/clipboard/_clipboard_sdl2.cpython-310-x86_64-linux-gnu.so
Executable file
Binary file not shown.
91
kivy/core/clipboard/clipboard_android.py
Normal file
91
kivy/core/clipboard/clipboard_android.py
Normal 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)
|
||||
41
kivy/core/clipboard/clipboard_dbusklipper.py
Normal file
41
kivy/core/clipboard/clipboard_dbusklipper.py
Normal 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']
|
||||
26
kivy/core/clipboard/clipboard_dummy.py
Normal file
26
kivy/core/clipboard/clipboard_dummy.py
Normal 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())
|
||||
47
kivy/core/clipboard/clipboard_gtk3.py
Normal file
47
kivy/core/clipboard/clipboard_gtk3.py
Normal 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']
|
||||
44
kivy/core/clipboard/clipboard_nspaste.py
Normal file
44
kivy/core/clipboard/clipboard_nspaste.py
Normal 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',)
|
||||
67
kivy/core/clipboard/clipboard_pygame.py
Normal file
67
kivy/core/clipboard/clipboard_pygame.py
Normal 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
|
||||
36
kivy/core/clipboard/clipboard_sdl2.py
Normal file
36
kivy/core/clipboard/clipboard_sdl2.py
Normal 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']
|
||||
66
kivy/core/clipboard/clipboard_winctypes.py
Normal file
66
kivy/core/clipboard/clipboard_winctypes.py
Normal 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']
|
||||
29
kivy/core/clipboard/clipboard_xclip.py
Normal file
29
kivy/core/clipboard/clipboard_xclip.py
Normal 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)
|
||||
29
kivy/core/clipboard/clipboard_xsel.py
Normal file
29
kivy/core/clipboard/clipboard_xsel.py
Normal 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
83
kivy/core/gl/__init__.py
Normal 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
|
||||
BIN
kivy/core/gl/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/core/gl/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
1002
kivy/core/image/__init__.py
Normal file
1002
kivy/core/image/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
kivy/core/image/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/core/image/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/image/__pycache__/img_dds.cpython-310.pyc
Normal file
BIN
kivy/core/image/__pycache__/img_dds.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/image/__pycache__/img_ffpyplayer.cpython-310.pyc
Normal file
BIN
kivy/core/image/__pycache__/img_ffpyplayer.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/image/__pycache__/img_pil.cpython-310.pyc
Normal file
BIN
kivy/core/image/__pycache__/img_pil.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/image/__pycache__/img_pygame.cpython-310.pyc
Normal file
BIN
kivy/core/image/__pycache__/img_pygame.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/image/__pycache__/img_sdl2.cpython-310.pyc
Normal file
BIN
kivy/core/image/__pycache__/img_sdl2.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/image/__pycache__/img_tex.cpython-310.pyc
Normal file
BIN
kivy/core/image/__pycache__/img_tex.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/image/_img_sdl2.cpython-310-x86_64-linux-gnu.so
Executable file
BIN
kivy/core/image/_img_sdl2.cpython-310-x86_64-linux-gnu.so
Executable file
Binary file not shown.
40
kivy/core/image/img_dds.py
Normal file
40
kivy/core/image/img_dds.py
Normal 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)
|
||||
87
kivy/core/image/img_ffpyplayer.py
Normal file
87
kivy/core/image/img_ffpyplayer.py
Normal 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
123
kivy/core/image/img_pil.py
Normal 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)
|
||||
119
kivy/core/image/img_pygame.py
Normal file
119
kivy/core/image/img_pygame.py
Normal 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)
|
||||
66
kivy/core/image/img_sdl2.py
Normal file
66
kivy/core/image/img_sdl2.py
Normal 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)
|
||||
58
kivy/core/image/img_tex.py
Normal file
58
kivy/core/image/img_tex.py
Normal 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)
|
||||
135
kivy/core/spelling/__init__.py
Normal file
135
kivy/core/spelling/__init__.py
Normal 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)
|
||||
BIN
kivy/core/spelling/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/core/spelling/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/spelling/__pycache__/spelling_enchant.cpython-310.pyc
Normal file
BIN
kivy/core/spelling/__pycache__/spelling_enchant.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
50
kivy/core/spelling/spelling_enchant.py
Normal file
50
kivy/core/spelling/spelling_enchant.py
Normal 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
|
||||
64
kivy/core/spelling/spelling_osxappkit.py
Normal file
64
kivy/core/spelling/spelling_osxappkit.py
Normal 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
1016
kivy/core/text/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
kivy/core/text/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/core/text/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/text/__pycache__/markup.cpython-310.pyc
Normal file
BIN
kivy/core/text/__pycache__/markup.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/text/__pycache__/text_pango.cpython-310.pyc
Normal file
BIN
kivy/core/text/__pycache__/text_pango.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/text/__pycache__/text_pil.cpython-310.pyc
Normal file
BIN
kivy/core/text/__pycache__/text_pil.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/text/__pycache__/text_pygame.cpython-310.pyc
Normal file
BIN
kivy/core/text/__pycache__/text_pygame.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/text/__pycache__/text_sdl2.cpython-310.pyc
Normal file
BIN
kivy/core/text/__pycache__/text_sdl2.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/text/_text_sdl2.cpython-310-x86_64-linux-gnu.so
Executable file
BIN
kivy/core/text/_text_sdl2.cpython-310-x86_64-linux-gnu.so
Executable file
Binary file not shown.
879
kivy/core/text/markup.py
Normal file
879
kivy/core/text/markup.py
Normal 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('&', '&')
|
||||
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)]
|
||||
)
|
||||
BIN
kivy/core/text/text_layout.cpython-310-x86_64-linux-gnu.so
Executable file
BIN
kivy/core/text/text_layout.cpython-310-x86_64-linux-gnu.so
Executable file
Binary file not shown.
13
kivy/core/text/text_layout.pxd
Normal file
13
kivy/core/text/text_layout.pxd
Normal 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
|
||||
145
kivy/core/text/text_pango.py
Normal file
145
kivy/core/text/text_pango.py
Normal 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))
|
||||
60
kivy/core/text/text_pil.py
Normal file
60
kivy/core/text/text_pil.py
Normal 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
|
||||
117
kivy/core/text/text_pygame.py
Normal file
117
kivy/core/text/text_pygame.py
Normal 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
|
||||
50
kivy/core/text/text_sdl2.py
Normal file
50
kivy/core/text/text_sdl2.py
Normal 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
219
kivy/core/video/__init__.py
Normal 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)
|
||||
BIN
kivy/core/video/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/core/video/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/video/__pycache__/video_ffmpeg.cpython-310.pyc
Normal file
BIN
kivy/core/video/__pycache__/video_ffmpeg.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/video/__pycache__/video_ffpyplayer.cpython-310.pyc
Normal file
BIN
kivy/core/video/__pycache__/video_ffpyplayer.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/video/__pycache__/video_gstplayer.cpython-310.pyc
Normal file
BIN
kivy/core/video/__pycache__/video_gstplayer.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/video/__pycache__/video_null.cpython-310.pyc
Normal file
BIN
kivy/core/video/__pycache__/video_null.cpython-310.pyc
Normal file
Binary file not shown.
106
kivy/core/video/video_ffmpeg.py
Normal file
106
kivy/core/video/video_ffmpeg.py
Normal 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)
|
||||
446
kivy/core/video/video_ffpyplayer.py
Normal file
446
kivy/core/video/video_ffpyplayer.py
Normal 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)
|
||||
140
kivy/core/video/video_gstplayer.py
Normal file
140
kivy/core/video/video_gstplayer.py
Normal 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
|
||||
12
kivy/core/video/video_null.py
Normal file
12
kivy/core/video/video_null.py
Normal 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
2460
kivy/core/window/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
kivy/core/window/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/core/window/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user