diff --git a/CMakeLists.txt b/CMakeLists.txt index 37619c8..dd48701 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,7 +58,7 @@ else() message(FATAL_ERROR "The conan_toolchain file could not be found: ${CONAN_TOOLCHAIN}") endif() -project(apngasm-python VERSION 1.2.0) +project(apngasm-python VERSION 1.2.1) set(PY_VERSION_SUFFIX "") set(PY_FULL_VERSION ${PROJECT_VERSION}${PY_VERSION_SUFFIX}) diff --git a/README.md b/README.md index 71f189c..f6fc37d 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ Documentations: https://apngasm-python.readthedocs.io/en/latest/ pip install apngasm-python ``` +Optionally, you can also install `Pillow` and `numpy` +``` +pip install Pillow numpy +``` + ## Example usage The recommended usage is to `from apngasm_python.apngasm import APNGAsmBinder`, see [example/example_binder.py](example/example_binder.py) ```python diff --git a/conanfile.py b/conanfile.py index 974eb8a..1b8dedb 100644 --- a/conanfile.py +++ b/conanfile.py @@ -4,26 +4,31 @@ from conan.tools.cmake import CMake, CMakeToolchain, CMakeDeps, cmake_layout from conan.tools.apple import is_apple_os + class ApngasmRecipe(ConanFile): - settings = 'os', 'compiler', 'build_type', 'arch' - + settings = "os", "compiler", "build_type", "arch" + def requirements(self): self.requires("zlib/1.2.13") self.requires("libpng/1.6.40") - self.requires("boost/1.75.0") # https://github.com/conan-io/conan-center-index/issues/19704 + self.requires( + "boost/1.75.0" + ) # https://github.com/conan-io/conan-center-index/issues/19704 def build_requirements(self): self.build_requires("b2/4.10.1") - if not shutil.which('cmake'): + if not shutil.which("cmake"): self.tool_requires("cmake/[>=3.27]") - + def build(self): - build_type = 'Release' - + build_type = "Release" + def generate(self): tc = CMakeToolchain(self) cmake = CMakeDeps(self) - if is_apple_os(self) and get_arch() == 'universal2': - tc.blocks['apple_system'].values['cmake_osx_architectures'] = 'x86_64; arm64' + if is_apple_os(self) and get_arch() == "universal2": + tc.blocks["apple_system"].values[ + "cmake_osx_architectures" + ] = "x86_64; arm64" tc.generate() - cmake.generate() \ No newline at end of file + cmake.generate() diff --git a/docs/conf.py b/docs/conf.py index bec27c8..4609efd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,19 +7,19 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information import os -project = 'apngasm-python' -copyright = '2023, laggykiller' -author = 'laggykiller' +project = "apngasm-python" +copyright = "2023, laggykiller" +author = "laggykiller" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ - 'autoapi.extension', - 'sphinx.ext.mathjax', - 'sphinx.ext.napoleon', - 'myst_parser', - 'sphinx_immaterial' + "autoapi.extension", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", + "myst_parser", + "sphinx_immaterial", ] autosummary_generate = True @@ -28,20 +28,20 @@ napoleon_numpy_docstring = True napoleon_use_ivar = True -templates_path = ['_templates'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] autoapi_dirs = [ - os.path.abspath('../src-python/apngasm_python'), - os.path.abspath('../src-python/apngasm_python/_apngasm_python') + os.path.abspath("../src-python/apngasm_python"), + os.path.abspath("../src-python/apngasm_python/_apngasm_python"), ] autoapi_python_use_implicit_namespaces = True # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'sphinx_immaterial' -html_static_path = ['_static'] +html_theme = "sphinx_immaterial" +html_static_path = ["_static"] # material theme options (see theme.conf for more information) html_theme_options = { "repo_name": "rlottie-python", @@ -85,4 +85,4 @@ }, ], "toc_title_is_page_title": True, -} \ No newline at end of file +} diff --git a/docs/requirements.txt b/docs/requirements.txt index 25e759b..0a40d08 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,5 @@ myst_parser sphinx-immaterial -sphinx-autoapi \ No newline at end of file +sphinx-autoapi +numpy +Pillow \ No newline at end of file diff --git a/example/example_binder.py b/example/example_binder.py index 4be8617..96f357e 100755 --- a/example/example_binder.py +++ b/example/example_binder.py @@ -6,80 +6,82 @@ import numpy as np # Cleanup -shutil.rmtree('output', ignore_errors=True) -os.mkdir('output') +shutil.rmtree("output", ignore_errors=True) +os.mkdir("output") # Initialize apngasm = APNGAsmBinder() # Get libapngasm version -print(f'{apngasm.version() = }') +print(f"{apngasm.version() = }") # Load png from one directory -for file_name in sorted(os.listdir('frames')): - apngasm.add_frame_from_file(os.path.join('frames', file_name), 100, 1000) +for file_name in sorted(os.listdir("frames")): + apngasm.add_frame_from_file(os.path.join("frames", file_name), 100, 1000) # Getting information about one frame frame = apngasm.get_frames()[0] # Saving one frame as file -frame.save('output/elephant-frame.png') +frame.save("output/elephant-frame.png") # Getting one frame as Pillow Image im = apngasm.frame_pixels_as_pillow(0) -im.save('output/elephant-frame-pillow.png') +im.save("output/elephant-frame-pillow.png") # Get inforamtion about whole animation -print(f'{apngasm.get_loops() = }') -print(f'{apngasm.is_skip_first() = }') -print(f'{apngasm.frame_count() = }') +print(f"{apngasm.get_loops() = }") +print(f"{apngasm.is_skip_first() = }") +print(f"{apngasm.frame_count() = }") # Assemble -success = apngasm.assemble('output/elephant.apng') -print(f'{success = }') +success = apngasm.assemble("output/elephant.apng") +print(f"{success = }") # Clear images loaded in apngasm object apngasm.reset() # Disassemble and get pillow image of one frame -frames = apngasm.disassemble_as_pillow('input/ball.apng') +frames = apngasm.disassemble_as_pillow("input/ball.apng") frame = frames[0] -frame.save('output/ball0.png') +frame.save("output/ball0.png") # Disassemble all APNG into PNGs -apngasm.save_pngs('output') +apngasm.save_pngs("output") # Assemble from pillow images # Just for fun, let's also make it spin apngasm.reset() angle = 0 -angle_step = 360 / len(os.listdir('frames')) -for file_name in sorted(os.listdir('frames')): - image = Image.open(os.path.join('frames', file_name)) +angle_step = 360 / len(os.listdir("frames")) +for file_name in sorted(os.listdir("frames")): + image = Image.open(os.path.join("frames", file_name)) image = image.rotate(angle) apngasm.add_frame_from_pillow(image) - + angle += angle_step -success = apngasm.assemble('output/elephant-spinning-pillow.apng') -print(f'{success = }') +success = apngasm.assemble("output/elephant-spinning-pillow.apng") +print(f"{success = }") apngasm.reset() # Assemble palette and grey PNGs # You can use with statement to avoid calling reset() with APNGAsmBinder() as apng: - apng.add_frame_from_file('input/palette.png', delay_num=1, delay_den=1) - apng.add_frame_from_file('input/grey.png', delay_num=1, delay_den=1) - success = apng.assemble('output/birds.apng') - print(f'{success = }') + apng.add_frame_from_file("input/palette.png", delay_num=1, delay_den=1) + apng.add_frame_from_file("input/grey.png", delay_num=1, delay_den=1) + success = apng.assemble("output/birds.apng") + print(f"{success = }") # Assemble palette and grey PNGs, but with Pillow and numpy -image0 = Image.open('input/grey.png') +image0 = Image.open("input/grey.png") frame0 = apngasm.add_frame_from_pillow(image0, delay_num=1, delay_den=1) -image1 = Image.open('input/grey.png').convert('RGB') -frame1 = apngasm.add_frame_from_numpy(np.array(image1), trns_color=np.array([255, 255, 255]), delay_num=1, delay_den=1) -image2 = Image.open('input/palette.png') +image1 = Image.open("input/grey.png").convert("RGB") +frame1 = apngasm.add_frame_from_numpy( + np.array(image1), trns_color=np.array([255, 255, 255]), delay_num=1, delay_den=1 +) +image2 = Image.open("input/palette.png") apngasm.add_frame_from_pillow(image2, delay_num=1, delay_den=1) -success = apngasm.assemble('output/birds-pillow.apng') -print(f'{success = }') \ No newline at end of file +success = apngasm.assemble("output/birds-pillow.apng") +print(f"{success = }") diff --git a/example/example_direct.py b/example/example_direct.py index 99a223c..2f6827e 100755 --- a/example/example_direct.py +++ b/example/example_direct.py @@ -1,103 +1,105 @@ #!/usr/bin/env python3 -from apngasm_python._apngasm_python import APNGAsm, APNGFrame, create_frame_from_rgb, create_frame_from_rgb_trns, create_frame_from_rgba +from apngasm_python._apngasm_python import ( + APNGAsm, + APNGFrame, + create_frame_from_rgb, + create_frame_from_rgb_trns, + create_frame_from_rgba, +) import os import shutil from PIL import Image import numpy as np + def frame_info(frame): - print(f'{frame.pixels = }') - print(f'{frame.width = }') - print(f'{frame.height = }') - print(f'{frame.color_type = }') - print(f'{frame.palette = }') - print(f'{frame.transparency = }') - print(f'{frame.palette_size = }') - print(f'{frame.transparency_size = }') - print(f'{frame.delay_num = }') - print(f'{frame.delay_den = }') + print(f"{frame.pixels = }") + print(f"{frame.width = }") + print(f"{frame.height = }") + print(f"{frame.color_type = }") + print(f"{frame.palette = }") + print(f"{frame.transparency = }") + print(f"{frame.palette_size = }") + print(f"{frame.transparency_size = }") + print(f"{frame.delay_num = }") + print(f"{frame.delay_den = }") + # https://www.w3.org/TR/PNG-Chunks.html -color_type_dict = { - 0: 'L', - 2: 'RGB', - 3: 'P', - 4: 'LA', - 6: 'RGBA' -} +color_type_dict = {0: "L", 2: "RGB", 3: "P", 4: "LA", 6: "RGBA"} color_type_dict.update(dict((v, k) for k, v in color_type_dict.items())) # Cleanup -shutil.rmtree('output', ignore_errors=True) -os.mkdir('output') +shutil.rmtree("output", ignore_errors=True) +os.mkdir("output") # Initialize apngasm = APNGAsm() # Get libapngasm version -print(f'{apngasm.version() = }') +print(f"{apngasm.version() = }") # Load png from one directory -for file_name in sorted(os.listdir('frames')): - apngasm.add_frame_from_file(os.path.join('frames', file_name), 100, 1000) +for file_name in sorted(os.listdir("frames")): + apngasm.add_frame_from_file(os.path.join("frames", file_name), 100, 1000) # Getting information about one frame frame = apngasm.get_frames()[0] frame_info(frame) # Saving one frame as file -frame.save('output/elephant-frame.png') +frame.save("output/elephant-frame.png") # Getting one frame as Pillow Image mode = color_type_dict[frame.color_type] im = Image.frombytes(mode, (frame.width, frame.height), frame.pixels) -im.save('output/elephant-frame-pillow.png') +im.save("output/elephant-frame-pillow.png") # Get inforamtion about whole animation -print(f'{apngasm.get_loops() = }') -print(f'{apngasm.is_skip_first() = }') -print(f'{apngasm.frame_count() = }') +print(f"{apngasm.get_loops() = }") +print(f"{apngasm.is_skip_first() = }") +print(f"{apngasm.frame_count() = }") # Assemble -success = apngasm.assemble('output/elephant.apng') -print(f'{success = }') +success = apngasm.assemble("output/elephant.apng") +print(f"{success = }") # Clear images loaded in apngasm object apngasm.reset() # Disassemble and get pillow image of one frame -frames = apngasm.disassemble('input/ball.apng') -print(f'{len(frames) = }') +frames = apngasm.disassemble("input/ball.apng") +print(f"{len(frames) = }") frame = frames[0] frame_info(frame) mode = color_type_dict[frame.color_type] im = Image.frombytes(mode, (frame.width, frame.height), frame.pixels) -im.save('output/ball0.png') +im.save("output/ball0.png") # Disassemble all APNG into PNGs -apngasm.save_pngs('output') +apngasm.save_pngs("output") # Assemble from pillow images # Just for fun, let's also make it spin apngasm.reset() angle = 0 -angle_step = 360 / len(os.listdir('frames')) -for file_name in sorted(os.listdir('frames')): - image = Image.open(os.path.join('frames', file_name)) +angle_step = 360 / len(os.listdir("frames")) +for file_name in sorted(os.listdir("frames")): + image = Image.open(os.path.join("frames", file_name)) image = image.rotate(angle) frame = create_frame_from_rgba(np.array(image), image.width, image.height) apngasm.add_frame(frame) - + angle += angle_step -success = apngasm.assemble('output/elephant-spinning-pillow.apng') -print(f'{success = }') +success = apngasm.assemble("output/elephant-spinning-pillow.apng") +print(f"{success = }") # Assemble palette and grey PNGs apngasm.reset() -apngasm.add_frame_from_file('input/palette.png', 100, 1000) -apngasm.add_frame_from_file('input/grey.png', 100, 1000) +apngasm.add_frame_from_file("input/palette.png", 100, 1000) +apngasm.add_frame_from_file("input/grey.png", 100, 1000) frame0 = apngasm.get_frames()[0] frame_info(frame0) @@ -105,22 +107,24 @@ def frame_info(frame): frame1 = apngasm.get_frames()[1] frame_info(frame1) -success = apngasm.assemble('output/birds.apng') -print(f'{success = }') +success = apngasm.assemble("output/birds.apng") +print(f"{success = }") del apngasm # Assemble palette and grey PNGs, but with Pillow -image0 = Image.open('input/grey.png').convert('RGB') +image0 = Image.open("input/grey.png").convert("RGB") frame0 = create_frame_from_rgb(np.array(image0), image0.width, image0.height, 1, 1) frame_info(frame0) -image1 = Image.open('input/grey.png').convert('RGB') -frame1 = create_frame_from_rgb_trns(np.array(image1), image0.width, image0.height, np.array([255, 255, 255]), 1, 1) +image1 = Image.open("input/grey.png").convert("RGB") +frame1 = create_frame_from_rgb_trns( + np.array(image1), image0.width, image0.height, np.array([255, 255, 255]), 1, 1 +) frame_info(frame1) # You may even set the variables manually -image2 = Image.open('input/palette.png').convert('RGBA') +image2 = Image.open("input/palette.png").convert("RGBA") frame2 = APNGFrame() frame2.delay_num = 1 frame2.delay_den = 1 @@ -133,5 +137,5 @@ def frame_info(frame): # Another way of creating APNGAsm object apngasm = APNGAsm([frame0, frame1, frame2]) -success = apngasm.assemble('output/birds-pillow.apng') -print(f'{success = }') \ No newline at end of file +success = apngasm.assemble("output/birds-pillow.apng") +print(f"{success = }") diff --git a/pyproject.toml b/pyproject.toml index e7bb812..8165d86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = { "file" = "LICENSE" } authors = [{ "name" = "chaudominic", "email" = "chaudominic2@gmail.com" }] keywords = ["apng", "png", "nanobind", "pybind11"] classifiers = ["Topic :: Multimedia :: Graphics"] -dependencies = ["Pillow", "numpy"] +dependencies = [] dynamic = ["version"] [project.urls] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e06245b..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Pillow -numpy \ No newline at end of file diff --git a/scripts/get_arch.py b/scripts/get_arch.py index 123bf74..8689e4f 100755 --- a/scripts/get_arch.py +++ b/scripts/get_arch.py @@ -4,17 +4,18 @@ import platform conan_archs = { - 'x86_64': ['amd64', 'x86_64', 'x64'], - 'x86': ['i386', 'i686', 'x86'], - 'armv8': ['arm64', 'aarch64', 'aarch64_be', 'armv8b', 'armv8l'], - 'ppc64le': ['ppc64le', 'powerpc'], - 's390x': ['s390', 's390x'] + "x86_64": ["amd64", "x86_64", "x64"], + "x86": ["i386", "i686", "x86"], + "armv8": ["arm64", "aarch64", "aarch64_be", "armv8b", "armv8l"], + "ppc64le": ["ppc64le", "powerpc"], + "s390x": ["s390", "s390x"], } + def get_arch(): arch = None - if os.getenv('APNGASM_COMPILE_TARGET'): - arch = os.getenv('APNGASM_COMPILE_TARGET') + if os.getenv("APNGASM_COMPILE_TARGET"): + arch = os.getenv("APNGASM_COMPILE_TARGET") else: for k, v in conan_archs.items(): if platform.machine().lower() in v: @@ -23,15 +24,17 @@ def get_arch(): if arch == None: arch = platform.machine().lower() - + return arch + def main(): arch = get_arch() - if os.getenv('APNGASM_COMPILE_TARGET'): - sys.stdout.write(os.getenv('APNGASM_COMPILE_TARGET')) + if os.getenv("APNGASM_COMPILE_TARGET"): + sys.stdout.write(os.getenv("APNGASM_COMPILE_TARGET")) else: sys.stdout.write(arch) -if __name__ == '__main__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() diff --git a/scripts/get_deps.py b/scripts/get_deps.py index afe2c96..64266d2 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -11,84 +11,98 @@ from scripts.get_arch import conan_archs, get_arch + def install_deps(arch): # Use Conan to install dependencies settings = [] - if platform.system() == 'Windows': - settings.append('os=Windows') - elif platform.system() == 'Darwin': - settings.append('os=Macos') - if arch == 'x86_64': - settings.append('os.version=10.15') + if platform.system() == "Windows": + settings.append("os=Windows") + elif platform.system() == "Darwin": + settings.append("os=Macos") + if arch == "x86_64": + settings.append("os.version=10.15") else: - settings.append('os.version=11.0') - settings.append('compiler=apple-clang') - settings.append('compiler.libcxx=libc++') - elif platform.system() == 'Linux': - settings.append('os=Linux') - settings.append('compiler=gcc') - settings.append('compiler.version=10') - settings.append('compiler.libcxx=libstdc++') + settings.append("os.version=11.0") + settings.append("compiler=apple-clang") + settings.append("compiler.libcxx=libc++") + elif platform.system() == "Linux": + settings.append("os=Linux") + settings.append("compiler=gcc") + settings.append("compiler.version=10") + settings.append("compiler.libcxx=libstdc++") if arch: - settings.append('arch=' + arch) + settings.append("arch=" + arch) build = [] - if (platform.system() == 'Linux'): + if platform.system() == "Linux": # Need to compile dependencies if Linux - build.append('*') - elif (not shutil.which('cmake') and - (platform.architecture()[0] == '32bit' or - platform.machine().lower() not in (conan_archs['armv8'] + conan_archs['x86']))): + build.append("*") + elif not shutil.which("cmake") and ( + platform.architecture()[0] == "32bit" + or platform.machine().lower() not in (conan_archs["armv8"] + conan_archs["x86"]) + ): + build.append("cmake*") - build.append('cmake*') - if build == []: - build.append('missing') - - print('conan cli settings:') - print('settings: ' + str(settings)) - print('build: ' + str(build)) - - subprocess.run(['conan', 'profile', 'detect']) - - conan_output = os.path.join('conan_output', arch) - - subprocess.run([ - 'conan', 'install', - *[x for s in settings for x in ('-s', s)], - *[x for b in build for x in ('-b', b)], - '-of', conan_output, '--deployer=direct_deploy', '.' - ]) - + build.append("missing") + + print("conan cli settings:") + print("settings: " + str(settings)) + print("build: " + str(build)) + + subprocess.run(["conan", "profile", "detect"]) + + conan_output = os.path.join("conan_output", arch) + + subprocess.run( + [ + "conan", + "install", + *[x for s in settings for x in ("-s", s)], + *[x for b in build for x in ("-b", b)], + "-of", + conan_output, + "--deployer=direct_deploy", + ".", + ] + ) + return conan_output + def main(): arch = get_arch() - if arch == 'universal2': - conan_output = 'conan_output/x86_64' + if arch == "universal2": + conan_output = "conan_output/x86_64" else: - conan_output = 'conan_output/' + arch + conan_output = "conan_output/" + arch if os.path.isdir(conan_output): - print('Dependencies found at:' + conan_output) - print('Skip conan install...') + print("Dependencies found at:" + conan_output) + print("Skip conan install...") return - if arch != 'universal2': + if arch != "universal2": conan_output = install_deps(arch) else: # Repeat to install the other architecture version of libwebp - conan_output_x64 = install_deps('x86_64') - conan_output_arm = install_deps('armv8') - conan_output_universal2 = conan_output_arm.replace('armv8', 'universal2') + conan_output_x64 = install_deps("x86_64") + conan_output_arm = install_deps("armv8") + conan_output_universal2 = conan_output_arm.replace("armv8", "universal2") shutil.rmtree(conan_output_universal2, ignore_errors=True) - subprocess.run([ - 'python3', 'lipo-dir-merge/lipo-dir-merge.py', - conan_output_x64, conan_output_arm, conan_output_universal2 - ]) + subprocess.run( + [ + "python3", + "lipo-dir-merge/lipo-dir-merge.py", + conan_output_x64, + conan_output_arm, + conan_output_universal2, + ] + ) shutil.rmtree(conan_output_x64) shutil.move(conan_output_universal2, conan_output_x64) -if __name__ == '__main__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() diff --git a/scripts/patch_stub.py b/scripts/patch_stub.py index 4f322ad..be93867 100755 --- a/scripts/patch_stub.py +++ b/scripts/patch_stub.py @@ -1,20 +1,24 @@ #!/usr/bin/env python3 import sys + def main(): init_pyi_path = sys.argv[1] with open(init_pyi_path) as f: init_pyi = f.read() - - with open(init_pyi_path, 'w+') as f: - init_pyi = init_pyi.replace('List[', 'list[') - if 'from __future__ import annotations' not in init_pyi: - f.write('from __future__ import annotations\n') - if 'import numpy.typing' not in init_pyi: - f.write('import numpy.typing\n') - if 'from . import _apngasm_python' not in init_pyi: - init_pyi = init_pyi.replace('import _apngasm_python', 'from . import _apngasm_python') + + with open(init_pyi_path, "w+") as f: + init_pyi = init_pyi.replace("List[", "list[") + if "from __future__ import annotations" not in init_pyi: + f.write("from __future__ import annotations\n") + if "import numpy.typing" not in init_pyi: + f.write("import numpy.typing\n") + if "from . import _apngasm_python" not in init_pyi: + init_pyi = init_pyi.replace( + "import _apngasm_python", "from . import _apngasm_python" + ) f.write(init_pyi) -if __name__ == '__main__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() diff --git a/scripts/update_stub.py b/scripts/update_stub.py index 5db4852..e127392 100755 --- a/scripts/update_stub.py +++ b/scripts/update_stub.py @@ -3,31 +3,33 @@ import shutil import zipfile + def main(): - py_bin = shutil.which('python3') + py_bin = shutil.which("python3") if not py_bin: - py_bin = shutil.which('python') - - dist_dir = os.path.join(os.path.split(os.path.abspath(__file__))[0], '../dist') + py_bin = shutil.which("python") + + dist_dir = os.path.join(os.path.split(os.path.abspath(__file__))[0], "../dist") shutil.rmtree(dist_dir, ignore_errors=True) - os.chdir(os.path.join(os.path.split(os.path.abspath(__file__))[0], '../')) - os.system(py_bin + ' -m build .') + os.chdir(os.path.join(os.path.split(os.path.abspath(__file__))[0], "../")) + os.system(py_bin + " -m build .") for zip_file in os.listdir(dist_dir): - if os.path.splitext(zip_file)[1] != '.whl': + if os.path.splitext(zip_file)[1] != ".whl": continue zip_path = os.path.join(dist_dir, zip_file) with zipfile.ZipFile(zip_path, mode="r") as archive: for file in archive.namelist(): - if os.path.splitext(file)[1] != '.pyi': + if os.path.splitext(file)[1] != ".pyi": continue - dest_path = os.path.join('src-python', file) + dest_path = os.path.join("src-python", file) dest_dir = os.path.split(dest_path)[0] if not os.path.isdir(dest_dir): os.makedirs(dest_dir) - with open(dest_path, 'wb+') as f: + with open(dest_path, "wb+") as f: f.write(archive.read(file)) -if __name__ == '__main__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() diff --git a/src-python/apngasm_python/__init__.py b/src-python/apngasm_python/__init__.py index 95a7685..90a626c 100755 --- a/src-python/apngasm_python/__init__.py +++ b/src-python/apngasm_python/__init__.py @@ -1,2 +1,3 @@ -'''apngasm-python''' -__version__ = '1.2.0' \ No newline at end of file +#!/usr/bin/env python3 +"""apngasm-python""" +__version__ = "1.2.1" diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index 1bb2ee6..8d60e60 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -1,104 +1,138 @@ #!/usr/bin/env python3 from __future__ import annotations -from ._apngasm_python import APNGAsm, APNGFrame, IAPNGAsmListener, create_frame_from_rgb, create_frame_from_rgb_trns, create_frame_from_rgba +from ._apngasm_python import ( + APNGAsm, + APNGFrame, + IAPNGAsmListener, + create_frame_from_rgb, + create_frame_from_rgb_trns, + create_frame_from_rgba, +) from ._apngasm_python import __version__ -import numpy as np -import numpy.typing -from PIL import Image + +try: + import numpy + import numpy.typing + + NUMPY_LOADED = True +except ModuleNotFoundError: + NUMPY_LOADED = False + +try: + from PIL import Image + + PILLOW_LOADED = True +except ModuleNotFoundError: + PILLOW_LOADED = False + from typing import Optional + class APNGAsmBinder: - ''' + """ Python class for binding apngasm library - ''' - + """ + # https://www.w3.org/TR/PNG-Chunks.html - color_type_dict = { - 0: 'L', - 2: 'RGB', - 3: 'P', - 4: 'LA', - 6: 'RGBA' - } + color_type_dict = {0: "L", 2: "RGB", 3: "P", 4: "LA", 6: "RGBA"} def __init__(self): self.apngasm = APNGAsm() - + def __enter__(self): return self - + def __exit__(self, exc_type, exc_val, exc_tb): self.apngasm.reset() - - def frame_pixels_as_pillow(self, frame: int, new_value: Optional[Image.Image] = None) -> Optional[Image.Image]: - ''' - Get/Set the raw pixel data of frame, expressed as a Pillow object. - This should be set AFTER you set the width, height and color_type. - - :param int frame: Target frame number. - :param Optional[PIL.Image.Image] new_value: If set, then the raw pixel data of frame - is set with this value. - - :return: Pillow image object of the frame (get) or None (set) - :rtype: Optional[PIL.Image.Image] - ''' - if new_value: - self.apngasm.get_frames()[frame].pixels = np.array(new_value) - else: - mode = self.color_type_dict[self.apngasm.get_frames()[frame].color_type] - return Image.frombytes(mode, (self.apngasm.get_frames()[frame].width, self.apngasm.get_frames()[frame].height), self.apngasm.get_frames()[frame].pixels) - - def frame_pixels_as_numpy(self, frame: int, new_value: Optional[numpy.typing.NDArray] = None) -> Optional[numpy.typing.NDArray]: - ''' - Get/Set the raw pixel data of frame, expressed as a 3D numpy array. - This should be set AFTER you set the width, height and color_type. - :param int frame: Target frame number. - :param Optional[numpy.typing.NDArray] new_value: If set, then the raw pixel data of frame - is set with this value. + if PILLOW_LOADED: + + def frame_pixels_as_pillow( + self, frame: int, new_value: Optional[Image.Image] = None + ) -> Optional[Image.Image]: + """ + Get/Set the raw pixel data of frame, expressed as a Pillow object. + This should be set AFTER you set the width, height and color_type. + + :param int frame: Target frame number. + :param Optional[PIL.Image.Image] new_value: If set, then the raw pixel data of frame + is set with this value. + + :return: Pillow image object of the frame (get) or None (set) + :rtype: Optional[PIL.Image.Image] + """ + if new_value: + self.apngasm.get_frames()[frame].pixels = numpy.array(new_value) + else: + mode = self.color_type_dict[self.apngasm.get_frames()[frame].color_type] + return Image.frombytes( + mode, + ( + self.apngasm.get_frames()[frame].width, + self.apngasm.get_frames()[frame].height, + ), + self.apngasm.get_frames()[frame].pixels, + ) + + if NUMPY_LOADED: + + def frame_pixels_as_numpy( + self, frame: int, new_value: Optional[numpy.typing.NDArray] = None + ) -> Optional[numpy.typing.NDArray]: + """ + Get/Set the raw pixel data of frame, expressed as a 3D numpy array. + This should be set AFTER you set the width, height and color_type. + + :param int frame: Target frame number. + :param Optional[numpy.typing.NDArray] new_value: If set, then the raw pixel data of frame + is set with this value. + + :return: 3D numpy array representation of raw pixel data of frame (get) or None (set) + :rtype: Optional[numpy.typing.NDArray] + """ + if new_value: + self.apngasm.get_frames()[frame].pixels = new_value + else: + return self.apngasm.get_frames()[frame].pixels - :return: 3D numpy array representation of raw pixel data of frame (get) or None (set) - :rtype: Optional[numpy.typing.NDArray] - ''' - if new_value: - self.apngasm.get_frames()[frame].pixels = new_value - else: - return self.apngasm.get_frames()[frame].pixels - def frame_width(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: - ''' + """ Get/Set the width of frame. - + :param int frame: Target frame number. - :param Optional[int] new_value: If set, then the width of frame + :param Optional[int] new_value: If set, then the width of frame is set with this value. :return: width (get) or None (set) :rtype: Optional[int] - ''' + """ if new_value: self.apngasm.get_frames()[frame].width = new_value else: return self.apngasm.get_frames()[frame].width - - def frame_height(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: - ''' + + def frame_height( + self, frame: int, new_value: Optional[int] = None + ) -> Optional[int]: + """ Get/Set the height of frame. - + :param int frame: Target frame number. - :param Optional[int] new_value: If set, then the height of frame + :param Optional[int] new_value: If set, then the height of frame is set with this value. :return: height (get) or None (set) :rtype: Optional[int] - ''' + """ if new_value: self.apngasm.get_frames()[frame].height = new_value else: return self.apngasm.get_frames()[frame].height - - def frame_color_type(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: - ''' + + def frame_color_type( + self, frame: int, new_value: Optional[int] = None + ) -> Optional[int]: + """ Get/Set the color_type of frame. 0: Grayscale (Pillow mode='L') @@ -106,278 +140,316 @@ def frame_color_type(self, frame: int, new_value: Optional[int] = None) -> Optio 3: Palette (Pillow mode='P') 4: Grayscale + Alpha (Pillow mode='LA') 6: RGBA (Pillow mode='RGBA') - + :param int frame: Target frame number. - :param Optional[int] new_value: If set, then the color type of frame + :param Optional[int] new_value: If set, then the color type of frame is set with this value. - + :return: color_type of frame (get) or None (set) :rtype: Optional[int] - ''' + """ if new_value: self.apngasm.get_frames()[frame].color_type = new_value else: return self.apngasm.get_frames()[frame].color_type - - def frame_palette(self, frame: int, new_value: Optional[numpy.typing.NDArray] = None) -> Optional[numpy.typing.NDArray]: - ''' - Get/Set the palette data of frame. Only applies to 'P' mode Image (i.e. Not RGB, RGBA) - Expressed as 2D numpy array in format of [[r0, g0, b0], [r1, g1, b1], ..., [r255, g255, b255]] - - :param int frame: Target frame number. - :param Optional[numpy.typing.NDArray] new_value: If set, then the palette data of frame - is set with this value. - :return: 2D numpy array representation of palette data of frame (get) or None (set) - :rtype: Optional[numpy.typing.NDArray] - ''' - if new_value: - self.apngasm.get_frames()[frame].palette = new_value - else: - return self.apngasm.get_frames()[frame].palette + if NUMPY_LOADED: - def frame_transparency(self, frame: int, new_value: Optional[numpy.typing.NDArray] = None) -> Optional[numpy.typing.NDArray]: - ''' - Get/Set the color [r, g, b] to be treated as transparent in the frame, expressed as 1D numpy array. - For more info, refer to 'tRNS Transparency' in http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html - - :param int frame: Target frame number. - :param Optional[numpy.typing.NDArray] new_value: If set, then the transparency of frame - is set with this value. + def frame_palette( + self, frame: int, new_value: Optional[numpy.typing.NDArray] = None + ) -> Optional[numpy.typing.NDArray]: + """ + Get/Set the palette data of frame. Only applies to 'P' mode Image (i.e. Not RGB, RGBA) + Expressed as 2D numpy array in format of [[r0, g0, b0], [r1, g1, b1], ..., [r255, g255, b255]] - :return: The color [r, g, b] to be treated as transparent in the frame (get) or None (set) - :rtype: Optional[numpy.typing.NDArray] - ''' - if new_value: - self.apngasm.get_frames()[frame].transparency = new_value - else: - return self.apngasm.get_frames()[frame].transparency + :param int frame: Target frame number. + :param Optional[numpy.typing.NDArray] new_value: If set, then the palette data of frame + is set with this value. - def frame_palette_size(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: - ''' + :return: 2D numpy array representation of palette data of frame (get) or None (set) + :rtype: Optional[numpy.typing.NDArray] + """ + if new_value: + self.apngasm.get_frames()[frame].palette = new_value + else: + return self.apngasm.get_frames()[frame].palette + + def frame_transparency( + self, frame: int, new_value: Optional[numpy.typing.NDArray] = None + ) -> Optional[numpy.typing.NDArray]: + """ + Get/Set the color [r, g, b] to be treated as transparent in the frame, expressed as 1D numpy array. + For more info, refer to 'tRNS Transparency' in + http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html + + :param int frame: Target frame number. + :param Optional[numpy.typing.NDArray] new_value: If set, then the transparency of frame + is set with this value. + + :return: The color [r, g, b] to be treated as transparent in the frame (get) or None (set) + :rtype: Optional[numpy.typing.NDArray] + """ + if new_value: + self.apngasm.get_frames()[frame].transparency = new_value + else: + return self.apngasm.get_frames()[frame].transparency + + def frame_palette_size( + self, frame: int, new_value: Optional[int] = None + ) -> Optional[int]: + """ Get/Set the palette data size of frame. - + :param int frame: Target frame number. - :param Optional[int] new_value: If set, then the palette data size of frame + :param Optional[int] new_value: If set, then the palette data size of frame is set with this value. :return: Palette data size of frame (get) or None (set) :rtype: Optional[int] - ''' + """ if new_value: self.apngasm.get_frames()[frame].palette_size = new_value else: return self.apngasm.get_frames()[frame].palette_size - - def frame_transparency_size(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: - ''' + + def frame_transparency_size( + self, frame: int, new_value: Optional[int] = None + ) -> Optional[int]: + """ Get/Set the transparency data size of frame. - + :param int frame: Target frame number. - :param Optional[int] new_value: If set, then the transparency data size of frame + :param Optional[int] new_value: If set, then the transparency data size of frame is set with this value. - + :return: Transparency data size of frame (get) or None (set) :rtype: Optional[int] - ''' + """ if new_value: self.apngasm.get_frames()[frame].transparency_size = new_value else: return self.apngasm.get_frames()[frame].transparency_size - - def frame_delay_num(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: - ''' + + def frame_delay_num( + self, frame: int, new_value: Optional[int] = None + ) -> Optional[int]: + """ Get/Set the nominator of the duration of frame. Duration of time is delay_num / delay_den seconds. - + :param int frame: Target frame number. :param Optional[int] new_value: If set, then the nominator of the duration of frame is set with this value. - + :return: Nominator of the duration of frame. :rtype: Optional[int] - ''' + """ if new_value: self.apngasm.get_frames()[frame].delay_num = new_value else: return self.apngasm.get_frames()[frame].delay_num - - def frame_delay_den(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: - ''' + + def frame_delay_den( + self, frame: int, new_value: Optional[int] = None + ) -> Optional[int]: + """ Get/Set the denominator of the duration of frame. Duration of time is delay_num / delay_den seconds. - + :param int frame: Target frame number. :param Optional[int] new_value: If set, then the denominator of the duration of frame is set with this value. - + :return: Denominator of the duration of frame. :rtype: Optional[int] - ''' + """ if new_value: self.apngasm.get_frames()[frame].delay_den = new_value else: return self.apngasm.get_frames()[frame].delay_den - - def add_frame_from_file(self, file_path: str, delay_num: int = 100, delay_den: int = 1000) -> int: - ''' + + def add_frame_from_file( + self, file_path: str, delay_num: int = 100, delay_den: int = 1000 + ) -> int: + """ Adds a frame from a PNG file or frames from a APNG file to the frame vector. - + :param str file_path: The relative or absolute path to an image file. :param int delay_num: The delay numerator for this frame (defaults to 100). :param int delay_den: The delay denominator for this frame (defaults to 1000). - + :return: The new number of frames. :rtype: int - ''' + """ return self.apngasm.add_frame_from_file( - file_path=file_path, - delay_num=delay_num, - delay_den=delay_den + file_path=file_path, delay_num=delay_num, delay_den=delay_den ) - - def add_frame_from_pillow(self, pillow_image: Image.Image, delay_num: int = 100, delay_den: int = 1000) -> int: - ''' - Add a frame from Pillow image. - The frame duration is equal to delay_num / delay_den seconds. - Default frame duration is 100/1000 second, or 0.1 second. - - :param PIL.Image.Image pillow_image: Pillow image object. - :param int delay_num: The delay numerator for this frame (defaults to 100). - :param int delay_den: The delay denominator for this frame (defaults to 1000). - - :return: The new number of frames. - :rtype: int - ''' - if pillow_image.mode not in ('RGB', 'RGBA'): - pillow_image = pillow_image.convert('RGBA') - return self.add_frame_from_numpy( - numpy_data=np.array(pillow_image), - width=pillow_image.width, - height=pillow_image.height, - mode=pillow_image.mode, - delay_num=delay_num, - delay_den=delay_den - ) - - def add_frame_from_numpy(self, numpy_data: numpy.typing.NDArray, width: Optional[int] = None, height: Optional[int] = None, trns_color: Optional[numpy.typing.NDArray] = None, - mode: Optional[str] = None, delay_num: int = 100, delay_den: int = 1000) -> int: - ''' - Add frame from numpy array. - The frame duration is equal to delay_num / delay_den seconds. - Default frame duration is 100/1000 second, or 0.1 second. - - :param numpy.typing.NDArray numpy_data: The pixel data, expressed as 3D numpy array. - :param Optional[int] width: The width of the pixel data. If not given, the 2nd dimension size of numpy_data is used. - :param Optional[int] height: The height of the pixel data. If not given, the 1st dimension size of numpy_data is used. - :param Optional[str] mode: The color mode of data. Possible values are RGB or RGBA. If not given, it is determined using the 3rd dimension size of numpy_data. - :param Optional[numpy.typing.NDArray] trns_color: The color [r, g, b] to be treated as transparent, expressed as 1D numpy array. - Only use if RGB mode. - :param int delay_num: The delay numerator for this frame (defaults to 100). - :param int delay_den: The delay denominator for this frame (defaults to 1000). - - :return: The new number of frames. - :rtype: int - ''' - width = width if width else np.shape(numpy_data)[1] - height = height if height else np.shape(numpy_data)[0] - - if not mode: - if np.shape(numpy_data)[2] == 3: - mode = 'RGB' - elif np.shape(numpy_data)[2] == 4: - mode = 'RGBA' - else: - raise TypeError('Cannot determine mode from numpy_data. ' - 'expected 3rd dimension size to be 3 (RGB) or 4 (RGBA). ' - f'The given numpy_data 3rd dimension size was {np.shape(numpy_data)[2]}.') - - if mode == 'RGB': - if type(trns_color) == numpy.typing.NDArray: - frame = create_frame_from_rgb_trns( + + if PILLOW_LOADED: + + def add_frame_from_pillow( + self, pillow_image: Image.Image, delay_num: int = 100, delay_den: int = 1000 + ) -> int: + """ + Add a frame from Pillow image. + The frame duration is equal to delay_num / delay_den seconds. + Default frame duration is 100/1000 second, or 0.1 second. + + :param PIL.Image.Image pillow_image: Pillow image object. + :param int delay_num: The delay numerator for this frame (defaults to 100). + :param int delay_den: The delay denominator for this frame (defaults to 1000). + + :return: The new number of frames. + :rtype: int + """ + if pillow_image.mode not in ("RGB", "RGBA"): + pillow_image = pillow_image.convert("RGBA") + return self.add_frame_from_numpy( + numpy_data=numpy.array(pillow_image), + width=pillow_image.width, + height=pillow_image.height, + mode=pillow_image.mode, + delay_num=delay_num, + delay_den=delay_den, + ) + + if NUMPY_LOADED: + + def add_frame_from_numpy( + self, + numpy_data: numpy.typing.NDArray, + width: Optional[int] = None, + height: Optional[int] = None, + trns_color: Optional[numpy.typing.NDArray] = None, + mode: Optional[str] = None, + delay_num: int = 100, + delay_den: int = 1000, + ) -> int: + """ + Add frame from numpy array. + The frame duration is equal to delay_num / delay_den seconds. + Default frame duration is 100/1000 second, or 0.1 second. + + :param numpy.typing.NDArray numpy_data: The pixel data, expressed as 3D numpy array. + :param Optional[int] width: The width of the pixel data. + If not given, the 2nd dimension size of numpy_data is used. + :param Optional[int] height: The height of the pixel data. + If not given, the 1st dimension size of numpy_data is used. + :param Optional[str] mode: The color mode of data. Possible values are RGB or RGBA. + If not given, it is determined using the 3rd dimension size of numpy_data. + :param Optional[numpy.typing.NDArray] trns_color: The color [r, g, b] to be treated as transparent, expressed as 1D numpy array. + Only use if RGB mode. + :param int delay_num: The delay numerator for this frame (defaults to 100). + :param int delay_den: The delay denominator for this frame (defaults to 1000). + + :return: The new number of frames. + :rtype: int + """ + width = width if width else numpy.shape(numpy_data)[1] + height = height if height else numpy.shape(numpy_data)[0] + + if not mode: + if numpy.shape(numpy_data)[2] == 3: + mode = "RGB" + elif numpy.shape(numpy_data)[2] == 4: + mode = "RGBA" + else: + raise TypeError( + "Cannot determine mode from numpy_data. " + "expected 3rd dimension size to be 3 (RGB) or 4 (RGBA). " + f"The given numpy_data 3rd dimension size was {numpy.shape(numpy_data)[2]}." + ) + + if mode == "RGB": + if type(trns_color) == numpy.typing.NDArray: + frame = create_frame_from_rgb_trns( + pixels=numpy_data, + width=width, + height=height, + trns_color=trns_color, + delay_num=delay_num, + delay_den=delay_den, + ) + else: + frame = create_frame_from_rgb( + pixels=numpy_data, + width=width, + height=height, + delay_num=delay_num, + delay_den=delay_den, + ) + elif mode == "RGBA": + if type(trns_color) == numpy.typing.NDArray: + raise TypeError( + "Cannot set trns_color on RGBA mode Pillow object. Must be RGB." + ) + frame = create_frame_from_rgba( pixels=numpy_data, width=width, height=height, - trns_color=trns_color, delay_num=delay_num, - delay_den=delay_den + delay_den=delay_den, ) else: - frame = create_frame_from_rgb( - pixels=numpy_data, - width=width, - height=height, - delay_num=delay_num, - delay_den=delay_den - ) - elif mode == 'RGBA': - if type(trns_color) == numpy.typing.NDArray: - raise TypeError('Cannot set trns_color on RGBA mode Pillow object. Must be RGB.') - frame = create_frame_from_rgba( - pixels=numpy_data, - width=width, - height=height, - delay_num=delay_num, - delay_den=delay_den - ) - else: - raise TypeError(f'Invalid mode: {mode}. Must be RGB or RGBA.') + raise TypeError(f"Invalid mode: {mode}. Must be RGB or RGBA.") - return self.apngasm.add_frame(frame) + return self.apngasm.add_frame(frame) def assemble(self, output_path: str) -> bool: - ''' + """ Assembles and outputs an APNG file. - + :param str output_path: The output file path. - + :return: true if assemble completed succesfully. :rtype: bool - ''' + """ return self.apngasm.assemble(output_path) - + def disassemble_as_numpy(self, file_path: str) -> list[APNGFrame]: - ''' + """ Disassembles an APNG file to a list of frames, expressed as 3D numpy array. - + :param str file_path: The file path to the PNG image to be disassembled. - + :return: A list containing the frames of the disassembled PNG. :rtype: list[apngasm_python._apngasm_python.APNGFrame] - ''' + """ return self.apngasm.disassemble(file_path) def disassemble_as_pillow(self, file_path: str) -> list[APNGFrame]: - ''' + """ Disassembles an APNG file to a list of frames, expressed as Pillow images. - + :param str file_path: The file path to the PNG image to be disassembled. - + :return: A list containing the frames of the disassembled PNG. :rtype: list[apngasm_python._apngasm_python.APNGFrame] - ''' + """ frames_numpy = self.apngasm.disassemble(file_path) frames_pillow = [] for frame in frames_numpy: mode = self.color_type_dict[frame.color_type] - frame_pillow = Image.frombytes(mode, (frame.width, frame.height), frame.pixels) + frame_pillow = Image.frombytes( + mode, (frame.width, frame.height), frame.pixels + ) frames_pillow.append(frame_pillow) - + return frames_pillow - + def save_pngs(self, output_dir: str) -> bool: - ''' + """ Saves individual PNG files of the frames in the frame vector. - + :param str output_dir: The directory where the PNG fils will be saved. - + :return: true if all files were saved successfully. :rtype: bool - ''' + """ return self.apngasm.save_pngs(output_dir) - + def load_animation_spec(self, file_path: str) -> list[APNGFrame]: - ''' + """ Loads an animation spec from JSON or XML. Loaded frames are added to the end of the frame vector. For more details on animation specs see: @@ -388,116 +460,115 @@ def load_animation_spec(self, file_path: str) -> list[APNGFrame]: :return: A vector containing the loaded frames. :rtype: list[apngasm_python._apngasm_python.APNGFrame] - ''' + """ return self.apngasm.load_animation_spec(file_path) - + def save_json(self, output_path: str, image_dir: str) -> bool: - ''' + """ Saves a JSON animation spec file. You probably won't need to use this function - + :param str output_path: Path to save the file to. :param str image_dir: Directory where frame files are to be saved if not the same path as the animation spec. - + :return: true if save was successful. :rtype: bool - ''' + """ return self.apngasm.save_json(output_path, image_dir) - + def save_xml(self, output_path: str, image_dir: str) -> bool: - ''' + """ Saves an XML animation spec file. - + :param str filePath: Path to save the file to. :param str image_dir: Directory where frame files are to be saved if not the same path as the animation spec. :return: true if save was successful. :rtype: bool - ''' + """ return self.apngasm.save_xml(output_path, image_dir) def set_apng_asm_listener(self, listener: Optional[IAPNGAsmListener] = None): - ''' + """ Sets a listener. You probably won't need to use this function. - + :param Optional[apngasm_python._apngasm_python.IAPNGAsmListener] listener: A pointer to the listener object. If the argument is None, a default APNGAsmListener will be created and assigned. - ''' + """ return self.apngasm.set_apng_asm_listener(listener) def set_loops(self, loops: int = 0): - ''' + """ Set loop count of animation. - + :param int loops: Loop count of animation. If the argument is 0 a loop count is infinity. - ''' + """ return self.apngasm.set_loops(loops) - + def set_skip_first(self, skip_first: int): - ''' + """ Set flag of skip first frame. :param int skip_first: Flag of skip first frame. - ''' + """ return self.apngasm.set_skip_first(skip_first) def get_frames(self) -> list[APNGFrame]: - ''' + """ Returns the frame vector. :return: frame vector. :rtype: list[apngasm_python._apngasm_python.APNGFrame] - ''' + """ return self.apngasm.get_frames() - + def get_loops(self) -> int: - ''' + """ Returns the loop count. :return: loop count. :rtype: int - ''' + """ return self.apngasm.get_loops() - + def is_skip_first(self) -> int: - ''' + """ Returns the flag of skip first frame. :return: flag of skip first frame. :rtype: int - ''' + """ return self.apngasm.get_loops() def frame_count(self) -> int: - ''' + """ Returns the number of frames. :return: number of frames. :rtype: int - ''' + """ return self.apngasm.frame_count() - + def reset(self) -> int: - ''' + """ Destroy all frames in memory/dispose of the frame vector. Leaves the apngasm object in a clean state. Returns number of frames disposed of. :return: number of frames disposed of. :rtype: int - ''' + """ return self.apngasm.reset() def version(self) -> str: - ''' + """ Returns the version of APNGAsm. :return: version of APNGAsm. :rtype: str - ''' + """ return self.apngasm.version() - diff --git a/src-python/apngasm_python/apngasm.pyi b/src-python/apngasm_python/apngasm.pyi index 1b4b580..89b08a5 100644 --- a/src-python/apngasm_python/apngasm.pyi +++ b/src-python/apngasm_python/apngasm.pyi @@ -4,6 +4,9 @@ from PIL import Image from _typeshed import Incomplete from typing import Optional +NUMPY_LOADED: bool +PILLOW_LOADED: bool + class APNGAsmBinder: color_type_dict: Incomplete apngasm: Incomplete