diff --git a/src/kaa/CMakeLists.txt b/src/kaa/CMakeLists.txt index 67be3dfb..bc1bf420 100644 --- a/src/kaa/CMakeLists.txt +++ b/src/kaa/CMakeLists.txt @@ -31,6 +31,7 @@ set(CYTHON_FILES shaders.pxi materials.pxi statistics.pxi + capture.pxi kaacore/__init__.pxd kaacore/engine.pxd @@ -62,6 +63,7 @@ set(CYTHON_FILES kaacore/resources.pxd kaacore/textures.pxd kaacore/statistics.pxd + kaacore/capture.pxd extra/include/pythonic_callback.h extra/include/python_exceptions_wrapper.h diff --git a/src/kaa/_kaa.pyx b/src/kaa/_kaa.pyx index 59d80eb8..5fa7e257 100644 --- a/src/kaa/_kaa.pyx +++ b/src/kaa/_kaa.pyx @@ -27,6 +27,7 @@ include "window.pxi" include "audio.pxi" include "timers.pxi" include "statistics.pxi" +include "capture.pxi" include "engine.pxi" include "shaders.pxi" include "materials.pxi" diff --git a/src/kaa/capture.pxi b/src/kaa/capture.pxi new file mode 100644 index 00000000..5db67bed --- /dev/null +++ b/src/kaa/capture.pxi @@ -0,0 +1,66 @@ +from libcpp.vector cimport vector +from cython.view cimport array as cvarray + +from .kaacore.capture cimport CCapturedFrames + +import base64 +from io import BytesIO + + +cdef class CapturedFrames: + cdef CCapturedFrames c_captured_frames + + cdef void c_attach_captured_frames(self, CCapturedFrames& c_captured_frames): + self.c_captured_frames = c_captured_frames + + @property + def memoryviews(self): + cdef cvarray image_array + cdef uint8_t* frame_data + cdef tuple dimensions = self.dimensions + cdef list collected_memoryviews = [] + + for frame_data in self.c_captured_frames.raw_ptr_frames_uint8(): + image_array = cvarray( + shape=dimensions, + itemsize=4, format='I', mode='c', allocate_buffer=False, + ) + image_array.data = frame_data + collected_memoryviews.append(image_array.get_memview()) + return collected_memoryviews + + @property + def dimensions(self): + return (self.c_captured_frames.width, self.c_captured_frames.height) + + +class HTMLBase64Image: + def __init__(self, bytes content, str image_type): + self.content_encoded = base64.b64encode(content).decode('ascii') + self.image_type = image_type + + def _repr_html_(self): + return ''.format( + self.image_type, self.content_encoded, + ) + + +def generate_gif(CapturedFrames captured_frames, *, duration=33): + from PIL import Image + + cdef object bytes_buffer = BytesIO() + cdef list images = [Image.frombuffer('RGBA', memview.shape, memview) + for memview in captured_frames.memoryviews] + + images[0].save( + bytes_buffer, format='gif', save_all=True, + append_images=images[1:], duration=duration, loop=0, + optimize=False, + ) + + bytes_buffer.seek(0) + return HTMLBase64Image(bytes_buffer.read(), 'image/gif') + + +def generate_auto(CapturedFrames captured_frames): + return generate_gif(captured_frames) diff --git a/src/kaa/engine.pxi b/src/kaa/engine.pxi index bcd2d62a..ac7b83a6 100644 --- a/src/kaa/engine.pxi +++ b/src/kaa/engine.pxi @@ -1,4 +1,5 @@ import atexit +import math from enum import IntEnum from contextlib import contextmanager @@ -12,11 +13,16 @@ from .kaacore.engine cimport ( CVirtualResolutionMode ) from .kaacore.display cimport CDisplay +from .kaacore.capture cimport CCapturedFrames from .kaacore.log cimport c_emit_log_dynamic, CLogLevel, _log_category_wrapper +from .kaacore.clock cimport CDuration from . import __version__ +cdef double DEFAULT_FIXED_FRAMETIME = 1. / 30. + + def _clean_up(): # force _c_engine_instance deletion, # so we are sure that kaacore dies before the python process @@ -99,6 +105,26 @@ cdef class _Engine: with nogil: c_engine.run(c_scene) + def run_capture( + self, Scene scene not None, uint32_t frames_limit, + *, double fixed_dt=DEFAULT_FIXED_FRAMETIME, + frames_preview_generator=None, + ): + cdef: + CScene* c_scene = scene._c_scene.get() + CEngine* c_engine = get_c_engine() + CDuration c_duration = CDuration(fixed_dt) + CCapturedFrames c_captured_frames + CapturedFrames captured_frames = CapturedFrames.__new__(CapturedFrames) + with nogil: + c_captured_frames = c_engine.run_capture(c_scene, frames_limit, c_duration) + + if frames_preview_generator is None: + frames_preview_generator = generate_auto + + captured_frames.c_attach_captured_frames(c_captured_frames) + return frames_preview_generator(captured_frames) + def quit(self): get_c_engine().quit() diff --git a/src/kaa/kaacore/capture.pxd b/src/kaa/kaacore/capture.pxd new file mode 100644 index 00000000..3a9afce9 --- /dev/null +++ b/src/kaa/kaacore/capture.pxd @@ -0,0 +1,13 @@ +from libcpp.memory cimport shared_ptr +from libcpp.vector cimport vector +from libc.stdint cimport uint8_t, uint32_t + +from .exceptions cimport raise_py_error + + +cdef extern from "kaacore/capture.h" namespace "kaacore" nogil: + cdef cppclass CCapturedFrames "kaacore::CapturedFrames": + uint32_t width + uint32_t height + + vector[uint8_t*] raw_ptr_frames_uint8() except +raise_py_error diff --git a/src/kaa/kaacore/engine.pxd b/src/kaa/kaacore/engine.pxd index d43f4a4c..be3c9b31 100644 --- a/src/kaa/kaacore/engine.pxd +++ b/src/kaa/kaacore/engine.pxd @@ -1,9 +1,10 @@ from libcpp.string cimport string from libcpp.memory cimport unique_ptr from libcpp.vector cimport vector -from libc.stdint cimport int32_t, uint64_t +from libc.stdint cimport uint32_t, uint64_t from .clock cimport CDuration +from .capture cimport CCapturedFrames from .display cimport CDisplay from .scenes cimport CScene from .window cimport CWindow @@ -47,6 +48,8 @@ cdef extern from "kaacore/engine.h" namespace "kaacore" nogil: double get_fps() except +raise_py_error void run(CScene* c_scene) except +raise_py_error + CCapturedFrames run_capture(CScene* c_scene, uint32_t frames_limit, + CDuration fixed_dt) except +raise_py_error void change_scene(CScene* c_scene) except +raise_py_error void quit() except +raise_py_error